/* Copyright (c) 2001-2017, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.lib.HsqlList;
import org.hsqldb.map.ValuePool;
import org.hsqldb.types.BinaryData;
import org.hsqldb.types.BinaryType;
import org.hsqldb.types.CharacterType;
import org.hsqldb.types.DateTimeType;
import org.hsqldb.types.IntervalType;
import org.hsqldb.types.Type;
import org.hsqldb.types.Types;

/**
 * Implementation of CAST, CASE, LIMIT and ZONE operations.
 *
 * @author Campbell Burnet (campbell-burnet@users dot sourceforge.net)
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.3.5
 * @since 1.9.0
 */
public class ExpressionOp extends Expression {

    static final ExpressionOp limitOneExpression = new ExpressionOp(
        OpTypes.LIMIT,
        new ExpressionValue(ValuePool.INTEGER_0, Type.SQL_INTEGER),
        new ExpressionValue(ValuePool.INTEGER_1, Type.SQL_INTEGER));

    /**
     * Creates a multiple arg operation expression
     */
    ExpressionOp(int type, Expression[] exprArray) {

        super(type);

        switch (opType) {

            case OpTypes.CONCAT_WS :
                nodes = exprArray;

                return;

            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "ExpressionOp");
        }
    }

    /**
     * Creates a special binary operation expression
     */
    ExpressionOp(int type, Expression left, Expression right) {

        super(type);

        nodes        = new Expression[BINARY];
        nodes[LEFT]  = left;
        nodes[RIGHT] = right;

        switch (opType) {

            case OpTypes.LIKE_ARG :
            case OpTypes.ALTERNATIVE :
            case OpTypes.CASEWHEN :
            case OpTypes.LIMIT :
            case OpTypes.ZONE_MODIFIER :
                return;

            case OpTypes.PREFIX :
                dataType = left.dataType;

                return;

            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "ExpressionOp");
        }
    }

    /**
     * creates a CAST expression
     */
    ExpressionOp(Expression e, Type dataType) {

        super(OpTypes.CAST);

        nodes         = new Expression[UNARY];
        nodes[LEFT]   = e;
        this.dataType = dataType;
        this.alias    = e.alias;
    }

    /**
     * creates a CONVERT expression with format
     * when format is null, it is a simple CAST
     */
    ExpressionOp(Expression e, Type dataType, Expression format) {

        super(OpTypes.CAST);

        nodes         = new Expression[UNARY];
        nodes[LEFT]   = e;
        this.dataType = dataType;
        this.alias    = e.alias;
    }

    /**
     * creates a special conversion for time / timestamp comparison
     */
    ExpressionOp(Expression e) {

        super(e.dataType.isDateTimeTypeWithZone() ? OpTypes.CAST
                                                  : OpTypes.ZONE_MODIFIER);

        switch (e.dataType.typeCode) {

            case Types.SQL_TIME_WITH_TIME_ZONE :
                nodes                = new Expression[UNARY];
                nodes[LEFT] = new ExpressionOp(OpTypes.ZONE_MODIFIER, e, null);
                nodes[LEFT].dataType = e.dataType;
                dataType = DateTimeType.getDateTimeType(Types.SQL_TIME,
                        e.dataType.scale);
                break;

            case Types.SQL_TIMESTAMP_WITH_TIME_ZONE :
                nodes                = new Expression[UNARY];
                nodes[LEFT] = new ExpressionOp(OpTypes.ZONE_MODIFIER, e, null);
                nodes[LEFT].dataType = e.dataType;
                dataType = DateTimeType.getDateTimeType(Types.SQL_TIMESTAMP,
                        e.dataType.scale);
                break;

            case Types.SQL_TIME :
                nodes                = new Expression[BINARY];
                nodes[LEFT]          = e;
                nodes[LEFT].dataType = e.dataType;
                dataType =
                    DateTimeType.getDateTimeType(Types.SQL_TIME_WITH_TIME_ZONE,
                                                 e.dataType.scale);
                break;

            case Types.SQL_TIMESTAMP :
                nodes                = new Expression[BINARY];
                nodes[LEFT]          = e;
                nodes[LEFT].dataType = e.dataType;
                dataType = DateTimeType.getDateTimeType(
                    Types.SQL_TIMESTAMP_WITH_TIME_ZONE, e.dataType.scale);
                break;

            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "ExpressionOp");
        }

        this.alias = e.alias;
    }

    public static Expression getCastExpression(Session session, Expression e,
            Type dataType) {

        if (e.getType() == OpTypes.VALUE) {
            Object value = dataType.castToType(session, e.getValue(session),
                                               e.getDataType());

            return new ExpressionValue(value, dataType);
        }

        return new ExpressionOp(e, dataType);
    }

    public String getSQL() {

        StringBuffer sb    = new StringBuffer(64);
        String       left  = getContextSQL(nodes.length > 0 ? nodes[LEFT]
                                                            : null);
        String       right = getContextSQL(nodes.length > 1 ? nodes[RIGHT]
                                                            : null);

        switch (opType) {

            case OpTypes.VALUE :
                if (valueData == null) {
                    return Tokens.T_NULL;
                }

                if (dataType == null) {
                    throw Error.runtimeError(ErrorCode.U_S0500,
                                             "ExpressionOp");
                }

                return dataType.convertToSQLString(valueData);

            case OpTypes.LIKE_ARG :
                sb.append(' ').append(Tokens.T_LIKE).append(' ');
                sb.append(left).append(' ').append(right).append(' ');
            case OpTypes.CAST :
                sb.append(' ').append(Tokens.T_CAST).append('(');
                sb.append(left).append(' ').append(Tokens.T_AS).append(' ');
                sb.append(dataType.getTypeDefinition());
                sb.append(')');

                return sb.toString();

            case OpTypes.CASEWHEN :
                sb.append(' ').append(Tokens.T_CASEWHEN).append('(');
                sb.append(left).append(',').append(right).append(')');

                return sb.toString();

            case OpTypes.ALTERNATIVE :
                sb.append(left).append(',').append(right);

                return sb.toString();

            case OpTypes.LIMIT :
                if (left != null) {
                    sb.append(' ').append(Tokens.T_OFFSET).append(' ');
                    sb.append(left).append(' ');
                }

                if (right != null) {
                    sb.append(' ').append(Tokens.T_FETCH).append(' ');
                    sb.append(Tokens.T_FIRST);
                    sb.append(right).append(' ').append(right).append(' ');
                    sb.append(Tokens.T_ROWS).append(' ').append(Tokens.T_ONLY);
                    sb.append(' ');
                }
                break;

            case OpTypes.ZONE_MODIFIER :
                sb.append(left).append(' ').append(Tokens.T_AT).append(' ');

                if (nodes[RIGHT] == null) {
                    sb.append(Tokens.T_LOCAL).append(' ');

                    break;
                }

                sb.append(Tokens.T_TIME).append(' ').append(Tokens.T_ZONE);
                sb.append(' ');
                sb.append(right);
                break;

            case OpTypes.CONCAT_WS :
                sb.append(Tokens.T_CONCAT_WS).append(Tokens.OPENBRACKET);
                sb.append(left);

                for (int i = 0; i < nodes.length; i++) {
                    sb.append(',').append(nodes[i].getSQL());
                }

                sb.append(Tokens.CLOSEBRACKET);
                break;

            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "ExpressionOp");
        }

        return sb.toString();
    }

    protected String describe(Session session, int blanks) {

        StringBuffer sb = new StringBuffer(64);

        sb.append('\n');

        for (int i = 0; i < blanks; i++) {
            sb.append(' ');
        }

        switch (opType) {

            case OpTypes.VALUE :
                sb.append("VALUE = ").append(
                    dataType.convertToSQLString(valueData));
                sb.append(", TYPE = ").append(dataType.getNameString());

                return sb.toString();

            case OpTypes.LIKE_ARG :
                sb.append(Tokens.T_LIKE).append(' ').append("ARG ");
                sb.append(dataType.getTypeDefinition());
                sb.append(' ');
                break;

            case OpTypes.VALUELIST :
                sb.append(Tokens.T_VALUE).append(' ').append("LIST ");

                for (int i = 0; i < nodes.length; i++) {
                    sb.append(nodes[i].describe(session, blanks + 1));
                    sb.append(' ');
                }

                return sb.toString();

            case OpTypes.CAST :
                sb.append(Tokens.T_CAST).append(' ');
                sb.append(dataType.getTypeDefinition());
                sb.append(' ');
                break;

            case OpTypes.CASEWHEN :
                sb.append(Tokens.T_CASEWHEN).append(' ');
                break;

            case OpTypes.CONCAT_WS :
                sb.append(Tokens.T_CONCAT_WS).append(' ');
                break;

            default :
        }

        if (getLeftNode() != null) {
            sb.append(" arg_left=[");
            sb.append(nodes[LEFT].describe(session, blanks + 1));
            sb.append(']');
        }

        if (getRightNode() != null) {
            sb.append(" arg_right=[");
            sb.append(nodes[RIGHT].describe(session, blanks + 1));
            sb.append(']');
        }

        return sb.toString();
    }

    public HsqlList resolveColumnReferences(Session session,
            RangeGroup rangeGroup, int rangeCount, RangeGroup[] rangeGroups,
            HsqlList unresolvedSet, boolean acceptsSequences) {

        if (opType == OpTypes.VALUE) {
            return unresolvedSet;
        }

        switch (opType) {

            case OpTypes.CASEWHEN :
                acceptsSequences = false;
                break;

            default :
        }

        for (int i = 0; i < nodes.length; i++) {
            if (nodes[i] == null) {
                continue;
            }

            unresolvedSet = nodes[i].resolveColumnReferences(session,
                    rangeGroup, rangeCount, rangeGroups, unresolvedSet,
                    acceptsSequences);
        }

        return unresolvedSet;
    }

    public void resolveTypes(Session session, Expression parent) {

        switch (opType) {

            case OpTypes.CASEWHEN :
                break;

            default :
                for (int i = 0; i < nodes.length; i++) {
                    if (nodes[i] != null) {
                        nodes[i].resolveTypes(session, this);
                    }
                }
        }

        switch (opType) {

            case OpTypes.VALUE :
                break;

            case OpTypes.LIKE_ARG : {
                dataType = nodes[LEFT].dataType;

                if (nodes[LEFT].opType == OpTypes.VALUE
                        && (nodes[RIGHT] == null
                            || nodes[RIGHT].opType == OpTypes.VALUE)) {
                    setAsConstantValue(session, parent);

                    break;
                }

                break;
            }
            case OpTypes.CAST : {
                Expression node     = nodes[LEFT];
                Type       nodeType = node.dataType;

                if (nodeType != null && !dataType.canConvertFrom(nodeType)) {
                    throw Error.error(ErrorCode.X_42561);
                }

                if (node.opType == OpTypes.VALUE) {
                    setAsConstantValue(session, parent);
                } else if (nodes[LEFT].opType == OpTypes.DYNAMIC_PARAM) {
                    node.dataType = dataType;
                }

                break;
            }
            case OpTypes.ZONE_MODIFIER :
                if (nodes[LEFT].dataType == null) {
                    throw Error.error(ErrorCode.X_42567);
                }

                if (nodes[RIGHT] != null) {
                    if (nodes[RIGHT].dataType == null) {
                        nodes[RIGHT].dataType =
                            Type.SQL_INTERVAL_HOUR_TO_MINUTE;
                    }

                    if (nodes[RIGHT].dataType.typeCode
                            != Types.SQL_INTERVAL_HOUR_TO_MINUTE) {
                        if (nodes[RIGHT].opType == OpTypes.VALUE) {
                            nodes[RIGHT].valueData =
                                Type.SQL_INTERVAL_HOUR_TO_MINUTE.castToType(
                                    session, nodes[RIGHT].valueData,
                                    nodes[RIGHT].dataType);
                            nodes[RIGHT].dataType =
                                Type.SQL_INTERVAL_HOUR_TO_MINUTE;
                        } else {
                            throw Error.error(ErrorCode.X_42563);
                        }
                    }
                }

                switch (nodes[LEFT].dataType.typeCode) {

                    case Types.SQL_TIME :
                        dataType = DateTimeType.getDateTimeType(
                            Types.SQL_TIME_WITH_TIME_ZONE,
                            nodes[LEFT].dataType.scale);
                        break;

                    case Types.SQL_TIMESTAMP :
                        dataType = DateTimeType.getDateTimeType(
                            Types.SQL_TIMESTAMP_WITH_TIME_ZONE,
                            nodes[LEFT].dataType.scale);
                        break;

                    case Types.SQL_TIMESTAMP_WITH_TIME_ZONE :
                    case Types.SQL_TIME_WITH_TIME_ZONE :
                        dataType = nodes[LEFT].dataType;
                        break;

                    default :
                        throw Error.error(ErrorCode.X_42563);
                }

                // no constant optimisation as value dependent on session zone
                break;

            case OpTypes.CASEWHEN :

                // We use CASEWHEN as parent type.
                // In the parent, left node is the condition, and right node is
                // the leaf, tagged as type ALTERNATIVE; its left node is
                // case 1 (how to get the value when the condition in
                // the parent evaluates to true), while its right node is case 2
                // (how to get the value when the condition in
                // the parent evaluates to false).
                resolveTypesForCaseWhen(session, parent);
                break;

            case OpTypes.CONCAT_WS :
                for (int i = 0; i < nodes.length; i++) {
                    nodes[i].dataType = Type.SQL_VARCHAR_DEFAULT;
                }

                dataType = Type.SQL_VARCHAR_DEFAULT;
                break;

            case OpTypes.ALTERNATIVE :
                resolveTypesForAlternative(session);
                break;

            case OpTypes.LIMIT :
                if (nodes[LEFT] != null) {
                    if (nodes[LEFT].dataType == null) {
                        throw Error.error(ErrorCode.X_42567);
                    }

                    if (!nodes[LEFT].dataType.isIntegralType()) {
                        throw Error.error(ErrorCode.X_42563);
                    }
                }

                if (nodes[RIGHT] != null) {
                    if (nodes[RIGHT].dataType == null) {
                        throw Error.error(ErrorCode.X_42567);
                    }

                    if (!nodes[RIGHT].dataType.isIntegralType()) {
                        throw Error.error(ErrorCode.X_42563);
                    }
                }
                break;

            case OpTypes.PREFIX :
                break;

            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "ExpressionOp");
        }
    }

    void resolveTypesForAlternative(Session session) {

        if (nodes[LEFT].dataType == null) {
            nodes[LEFT].dataType = nodes[RIGHT].dataType;
        }

        if (nodes[RIGHT].dataType == null) {
            nodes[RIGHT].dataType = nodes[LEFT].dataType;
        }

        if (exprSubType == OpTypes.CAST) {
            if (nodes[RIGHT].dataType == null) {
                nodes[RIGHT].dataType = nodes[LEFT].dataType =
                    Type.SQL_VARCHAR_DEFAULT;;
            }

            dataType = nodes[RIGHT].dataType;

            if (!nodes[RIGHT].dataType.equals(nodes[LEFT].dataType)) {
                if (dataType.isCharacterType()) {
                    dataType = Type.SQL_VARCHAR_DEFAULT;
                }

                nodes[LEFT] = new ExpressionOp(nodes[LEFT], dataType);
            }
        } else {
            dataType = Type.getAggregateType(nodes[LEFT].dataType, dataType);
            dataType = Type.getAggregateType(nodes[RIGHT].dataType, dataType);
        }
    }

    /**
     * For CASE WHEN and its special cases section 9.3 of the SQL standard
     * on type aggregation is implemented.
     */
    void resolveTypesForCaseWhen(Session session, Expression parent) {

        nodes[RIGHT].resolveTypes(session, this);

        Expression expr = this;

        while (expr.opType == OpTypes.CASEWHEN) {
            if (expr.exprSubType == OpTypes.CAST) {
                dataType = expr.nodes[RIGHT].dataType;
            } else {
                dataType = Type.getAggregateType(expr.nodes[RIGHT].dataType,
                                                 dataType);
            }

            if (expr.nodes[RIGHT].nodes[RIGHT].opType == OpTypes.CASEWHEN) {
                expr = expr.nodes[RIGHT].nodes[RIGHT];
            } else {
                expr = expr.nodes[RIGHT].nodes[LEFT];
            }
        }

        expr = this;

        while (expr.opType == OpTypes.CASEWHEN) {
            if (expr.nodes[RIGHT].dataType == null) {
                expr.nodes[RIGHT].dataType = dataType;
            }

            if (expr.nodes[RIGHT].nodes[RIGHT].dataType == null) {
                expr.nodes[RIGHT].nodes[RIGHT].dataType = dataType;
            }

            if (expr.nodes[RIGHT].nodes[LEFT].dataType == null) {
                expr.nodes[RIGHT].nodes[LEFT].dataType = dataType;
            }

            if (expr.nodes[RIGHT].nodes[RIGHT].opType == OpTypes.CASEWHEN) {
                expr = expr.nodes[RIGHT].nodes[RIGHT];
            } else {
                expr = expr.nodes[RIGHT].nodes[LEFT];
            }
        }

        expr = this;

        while (expr.opType == OpTypes.CASEWHEN) {
            expr.nodes[LEFT].resolveTypes(session, expr);

            if (expr.nodes[LEFT].isUnresolvedParam()) {
                expr.nodes[LEFT].dataType = Type.SQL_BOOLEAN;
            }

            expr.nodes[RIGHT].nodes[LEFT].resolveTypes(session,
                    expr.nodes[RIGHT]);

            if (expr.nodes[RIGHT].nodes[RIGHT].opType != OpTypes.CASEWHEN) {
                expr.nodes[RIGHT].nodes[RIGHT].resolveTypes(session,
                        expr.nodes[RIGHT]);
            }

            expr = expr.nodes[RIGHT].nodes[RIGHT];
        }

        if (parent == null || parent.opType != OpTypes.ALTERNATIVE) {
            if (dataType == null || dataType.typeCode == Types.SQL_ALL_TYPES) {
                throw Error.error(ErrorCode.X_42567);
            }
        }
    }

    public Object getValue(Session session) {

        switch (opType) {

            case OpTypes.VALUE :
                return valueData;

            case OpTypes.LIKE_ARG : {
                boolean hasEscape  = nodes[RIGHT] != null;
                int     escapeChar = Integer.MAX_VALUE;

                if (dataType.isBinaryType()) {
                    BinaryData left =
                        (BinaryData) nodes[LEFT].getValue(session);

                    if (left == null) {
                        return null;
                    }

                    if (hasEscape) {
                        BinaryData right =
                            (BinaryData) nodes[RIGHT].getValue(session);

                        if (right == null) {
                            return null;
                        }

                        if (right.length(session) != 1) {
                            throw Error.error(ErrorCode.X_2200D);
                        }

                        escapeChar = right.getBytes()[0];
                    }

                    byte[]  array       = left.getBytes();
                    byte[]  newArray    = new byte[array.length];
                    boolean wasEscape   = false;
                    int     escapeCount = 0;
                    int     i           = 0;
                    int     j           = 0;

                    for (; i < array.length; i++) {
                        if (array[i] == escapeChar) {
                            if (wasEscape) {
                                escapeCount++;

                                newArray[j++] = array[i];
                                wasEscape     = false;

                                continue;
                            }

                            wasEscape = true;

                            if (i == array.length - 1) {
                                throw Error.error(ErrorCode.X_22025);
                            }

                            continue;
                        }

                        if (array[i] == '_' || array[i] == '%') {
                            if (wasEscape) {
                                escapeCount++;

                                newArray[j++] = array[i];
                                wasEscape     = false;

                                continue;
                            }

                            break;
                        }

                        if (wasEscape) {
                            throw Error.error(ErrorCode.X_22025);
                        }

                        newArray[j++] = array[i];
                    }

                    newArray =
                        (byte[]) ArrayUtil.resizeArrayIfDifferent(newArray, j);

                    return new BinaryData(newArray, false);
                } else {
                    String left =
                        (String) Type.SQL_VARCHAR.convertToType(session,
                            nodes[LEFT].getValue(session),
                            nodes[LEFT].getDataType());

                    if (left == null) {
                        return null;
                    }

                    if (hasEscape) {
                        String right =
                            (String) Type.SQL_VARCHAR.convertToType(session,
                                nodes[RIGHT].getValue(session),
                                nodes[RIGHT].getDataType());

                        if (right == null) {
                            return null;
                        }

                        if (right.length() != 1) {
                            throw Error.error(ErrorCode.X_22019);
                        }

                        escapeChar = right.getBytes()[0];
                    }

                    char[]  array       = left.toCharArray();
                    char[]  newArray    = new char[array.length];
                    boolean wasEscape   = false;
                    int     escapeCount = 0;
                    int     i           = 0;
                    int     j           = 0;

                    for (; i < array.length; i++) {
                        if (array[i] == escapeChar) {
                            if (wasEscape) {
                                escapeCount++;

                                newArray[j++] = array[i];
                                wasEscape     = false;

                                continue;
                            }

                            wasEscape = true;

                            if (i == array.length - 1) {
                                throw Error.error(ErrorCode.X_22025);
                            }

                            continue;
                        }

                        if (array[i] == '_' || array[i] == '%') {
                            if (wasEscape) {
                                escapeCount++;

                                newArray[j++] = array[i];
                                wasEscape     = false;

                                continue;
                            }

                            break;
                        }

                        if (wasEscape) {
                            throw Error.error(ErrorCode.X_22025);
                        }

                        newArray[j++] = array[i];
                    }

                    return new String(newArray, 0, j);
                }
            }
            case OpTypes.ORDER_BY :
                return nodes[LEFT].getValue(session);

            case OpTypes.PREFIX : {
                if (nodes[LEFT].dataType.isCharacterType()) {
                    Object value = nodes[RIGHT].getValue(session);

                    if (value == null) {
                        return null;
                    }

                    CharacterType type = (CharacterType) nodes[RIGHT].dataType;
                    long length =
                        ((CharacterType) nodes[RIGHT].dataType).size(session,
                            value);

                    type  = (CharacterType) nodes[LEFT].dataType;
                    value = nodes[LEFT].getValue(session);

                    if (value == null) {
                        return null;
                    }

                    return type.substring(session, value, 0, length, true,
                                          false);
                } else {
                    BinaryData value =
                        (BinaryData) nodes[RIGHT].getValue(session);

                    if (value == null) {
                        return null;
                    }

                    long       length = value.length(session);
                    BinaryType type   = (BinaryType) nodes[LEFT].dataType;

                    value = (BinaryData) nodes[LEFT].getValue(session);

                    if (value == null) {
                        return null;
                    }

                    return type.substring(session, value, 0, length, true);
                }
            }
            case OpTypes.CAST : {
                Object value =
                    dataType.castToType(session,
                                        nodes[LEFT].getValue(session),
                                        nodes[LEFT].dataType);

                if (dataType.userTypeModifier != null) {
                    Constraint[] constraints =
                        dataType.userTypeModifier.getConstraints();

                    for (int i = 0; i < constraints.length; i++) {
                        constraints[i].checkCheckConstraint(session, null,
                                                            null, value);
                    }
                }

                return value;
            }
            case OpTypes.CASEWHEN : {
                Boolean result = (Boolean) nodes[LEFT].getValue(session);

                if (Boolean.TRUE.equals(result)) {
                    return nodes[RIGHT].nodes[LEFT].getValue(session,
                            dataType);
                } else {
                    return nodes[RIGHT].nodes[RIGHT].getValue(session,
                            dataType);
                }
            }
            case OpTypes.CONCAT_WS : {
                String sep = (String) nodes[LEFT].getValue(session);

                if (sep == null) {
                    return null;
                }

                StringBuffer sb       = new StringBuffer("");
                boolean      hasValue = false;

                for (int i = 1; i < nodes.length; i++) {
                    String value = (String) nodes[i].getValue(session);

                    if (value == null) {
                        continue;
                    }

                    if (hasValue) {
                        sb.append(sep);
                    }

                    sb.append(value);

                    hasValue = true;
                }

                return sb.toString();
            }
            case OpTypes.ZONE_MODIFIER : {
                Object leftValue  = nodes[LEFT].getValue(session);
                Object rightValue = nodes[RIGHT] == null ? null
                                                         : nodes[RIGHT]
                                                             .getValue(
                                                                 session);

                if (leftValue == null) {
                    return null;
                }

                if (nodes[RIGHT] != null && rightValue == null) {
                    return null;
                }

                long zoneSeconds = nodes[RIGHT] == null
                                   ? session.getZoneSeconds()
                                   : ((IntervalType) nodes[RIGHT].dataType)
                                       .getSeconds(rightValue);

                return ((DateTimeType) dataType).changeZone(leftValue,
                        nodes[LEFT].dataType, (int) zoneSeconds,
                        session.getZoneSeconds());
            }
            case OpTypes.LIMIT :

            // fall through
            default :
                throw Error.runtimeError(ErrorCode.U_S0500, "ExpressionOp");
        }
    }
}
