public static TypeTriple getReturnTypeOfDecimal(Operator op, ScalarType lhsType, ScalarType rhsType)
            throws SemanticException {
        Preconditions.checkState(lhsType.isDecimalV3() && rhsType.isDecimalV3(),
                "Types of lhs and rhs must be DecimalV3");
        final PrimitiveType lhsPtype = lhsType.getPrimitiveType();
        final PrimitiveType rhsPtype = rhsType.getPrimitiveType();
        final int lhsPrecision = lhsType.getPrecision();
        final int rhsPrecision = rhsType.getPrecision();
        final int lhsScale = lhsType.getScalarScale();
        final int rhsScale = rhsType.getScalarScale();

        // get the wider decimal type
        PrimitiveType widerType = PrimitiveType.getWiderDecimalV3Type(lhsPtype, rhsPtype);
        // compute arithmetic expr use decimal64 for both decimal32 and decimal64
        widerType = PrimitiveType.getWiderDecimalV3Type(widerType, PrimitiveType.DECIMAL64);
        int maxPrecision = PrimitiveType.getMaxPrecisionOfDecimal(widerType);

        TypeTriple result = new TypeTriple();
        result.lhsTargetType = ScalarType.createDecimalV3Type(widerType, maxPrecision, lhsScale);
        result.rhsTargetType = ScalarType.createDecimalV3Type(widerType, maxPrecision, rhsScale);
        int returnScale = 0;
        int returnPrecision = 0;
        switch (op) {
            case ADD:
            case SUBTRACT:
                getAddSubReturnTypeOfDecimal(result, lhsType, rhsType);
                return result;
            case MOD:
                returnScale = Math.max(lhsScale, rhsScale);
                break;
            case MULTIPLY:
                returnScale = lhsScale + rhsScale;
                returnPrecision = lhsPrecision + rhsPrecision;
                final int maxDecimalPrecision = PrimitiveType.getMaxPrecisionOfDecimal(PrimitiveType.DECIMAL128);
                if (returnPrecision <= maxDecimalPrecision) {
                    // returnPrecision <= 38, result never overflows, use the narrowest decimal type that can holds the result.
                    // for examples:
                    // decimal32(4,3) * decimal32(4,3) => decimal32(8,6);
                    // decimal64(15,3) * decimal32(9,4) => decimal128(24,7).
                    PrimitiveType commonPtype =
                            ScalarType.createDecimalV3NarrowestType(returnPrecision, returnScale).getPrimitiveType();
                    // a common type shall never be narrower than type of lhs and rhs
                    commonPtype = PrimitiveType.getWiderDecimalV3Type(commonPtype, lhsPtype);
                    commonPtype = PrimitiveType.getWiderDecimalV3Type(commonPtype, rhsPtype);
                    result.returnType = ScalarType.createDecimalV3Type(commonPtype, returnPrecision, returnScale);
                    result.lhsTargetType = ScalarType.createDecimalV3Type(commonPtype, lhsPrecision, lhsScale);
                    result.rhsTargetType = ScalarType.createDecimalV3Type(commonPtype, rhsPrecision, rhsScale);
                    return result;
                } else if (returnScale <= maxDecimalPrecision) {
                    // returnPrecision > 38 and returnScale <= 38, the multiplication is computable but the result maybe
                    // overflow, so use decimal128 arithmetic and adopt maximum decimal precision(38) as precision of
                    // the result.
                    // for examples:
                    // decimal128(23,5) * decimal64(18,4) => decimal128(38, 9).
                    result.returnType =
                            ScalarType.createDecimalV3Type(PrimitiveType.DECIMAL128, maxDecimalPrecision, returnScale);
                    result.lhsTargetType =
                            ScalarType.createDecimalV3Type(PrimitiveType.DECIMAL128, lhsPrecision, lhsScale);
                    result.rhsTargetType =
                            ScalarType.createDecimalV3Type(PrimitiveType.DECIMAL128, rhsPrecision, rhsScale);
                    return result;
                } else {
                    // returnScale > 38, so it is cannot be represented as decimal.
                    throw new SemanticException(
                            String.format(
                                    "Return scale(%d) exceeds maximum value(%d), please cast decimal type to low-precision one",
                                    returnScale, maxDecimalPrecision));
                }
            case INT_DIVIDE:
            case DIVIDE:
                if (lhsScale <= 6) {
                    returnScale = lhsScale + 6;
                } else if (lhsScale <= 12) {
                    returnScale = 12;
                } else {
                    returnScale = lhsScale;
                }
                widerType = PrimitiveType.DECIMAL128;
                maxPrecision = PrimitiveType.getMaxPrecisionOfDecimal(widerType);
                result.lhsTargetType = ScalarType.createDecimalV3Type(widerType, maxPrecision, lhsScale);
                result.rhsTargetType = ScalarType.createDecimalV3Type(widerType, maxPrecision, rhsScale);
                int adjustedScale = returnScale + rhsScale;
                if (adjustedScale > maxPrecision) {
                    throw new SemanticException(
                            String.format(
                                    "Dividend fails to adjust scale to %d that exceeds maximum value(%d)",
                                    adjustedScale,
                                    maxPrecision));
                }
                break;
            case BITAND:
            case BITOR:
            case BITXOR:
            case BIT_SHIFT_LEFT:
            case BIT_SHIFT_RIGHT:
            case BIT_SHIFT_RIGHT_LOGICAL:
                result.lhsTargetType = ScalarType.BIGINT;
                result.rhsTargetType = ScalarType.BIGINT;
                result.returnType = ScalarType.BIGINT;
                return result;
            default:
                Preconditions.checkState(false, "DecimalV3 only support operators: +-*/%&|^");
        }
        result.returnType = op == Operator.INT_DIVIDE ? ScalarType.BIGINT :
                ScalarType.createDecimalV3Type(widerType, maxPrecision, returnScale);
        return result;
    }
@Test
    public void testDecimalMultiplyFail() {
        Object[][] cases = new Object[][] {
                {
                        dec(128, 38, 36),
                        dec(32, 4, 3),
                },
                {
                        dec(128, 37, 24),
                        dec(64, 18, 15),
                },
                {
                        dec(128, 30, 19),
                        dec(128, 30, 20),
                },
        };
        for (Object[] c : cases) {
            ScalarType lhsType = (ScalarType) c[0];
            ScalarType rhsType = (ScalarType) c[1];
            try {
                ArithmeticExpr.getReturnTypeOfDecimal(ArithmeticExpr.Operator.MULTIPLY, lhsType, rhsType);
                Assert.fail("should throw exception");
            } catch (SemanticException ignored) {
            }
        }
    }