Repository: pingcap/tidb (Go)

Issue:
varbinary + int should be double
## Bug Report

Please answer these questions before submitting your issue. Thanks!

### 1. Minimal reproduce step (Required)

<!-- a step by step guide for reproducing the bug. -->
``` SQL
create table tg(a varbinary(10));
insert into tg values ("123458.12");
select 1+a from tg;
```

### 2. What did you expect to see? (Required)
123459.12
### 3. What did you see instead (Required)
123459
### 4. What is your TiDB version? (Required)
master
<!-- Paste the output of SELECT tidb_version() -->




Discussion:
The problem is that, in numericContextResultType(), if types.IsBinaryStr(ft) then int. It's wrong. We should distinguish hex literal from varbinary.
/assign
Do you mean if there is hex literal in varbinary, we should return types.ETDecimal? @wjhuang2016 
> Do you mean if there is hex literal in varbinary, we should return types.ETDecimal? @wjhuang2016

No, it should be ETReal
> > Do you mean if there is hex literal in varbinary, we should return types.ETDecimal? @wjhuang2016
> 
> No, it should be ETReal

Yeah, it should be ETReal. But how to know whether there is hex literal in varbinary?
I think we can use a flag(like IsBooleanFlag), what do you think? @XuHuaiyu 
tidb casts `tg.a` to type bigint and compute result. At least it should be casted to decimal.

```
MySQL [test]> select 1+a from tg;
+--------+
| 1+a    |
+--------+
| 123459 |
+--------+
1 row in set, 1 warning (0.00 sec)

MySQL [test]> show warnings;
+---------+------+------------------------------------------------+
| Level   | Code | Message                                        |
+---------+------+------------------------------------------------+
| Warning | 1292 | Truncated incorrect INTEGER value: '123458.12' |
+---------+------+------------------------------------------------+
1 row in set (0.00 sec)
```

```
MySQL [test]> explain select 1+a from tg;
+-------------------------+---------+-----------+---------------+-------------------------------------------------------+
| id                      | estRows | task      | access object | operator info                                         |
+-------------------------+---------+-----------+---------------+-------------------------------------------------------+
| Projection_3            | 1.00    | root      |               | plus(1, cast(test.tg.a, bigint(10) BINARY))->Column#3 |
| └─TableReader_5         | 1.00    | root      |               | data:TableFullScan_4                                  |
|   └─TableFullScan_4     | 1.00    | cop[tikv] | table:tg      | keep order:false, stats:pseudo                        |
+-------------------------+---------+-----------+---------------+-------------------------------------------------------+
3 rows in set (0.00 sec)
```





Patch:
diff --git a/expression/builtin_arithmetic.go b/expression/builtin_arithmetic.go
index 29579fb06a3f1..067b5c2b50d76 100644
--- a/expression/builtin_arithmetic.go
+++ b/expression/builtin_arithmetic.go
@@ -63,16 +63,32 @@ var (
 // performed with the / operator.
 const precIncrement = 4
 
+// isConstantBinaryLiteral return true if expr is constant binary literal
+func isConstantBinaryLiteral(expr Expression) bool {
+	if types.IsBinaryStr(expr.GetType()) {
+		if v, ok := expr.(*Constant); ok {
+			if k := v.Value.Kind(); k == types.KindBinaryLiteral {
+				return true
+			}
+		}
+	}
+	return false
+}
+
 // numericContextResultType returns types.EvalType for numeric function's parameters.
 // the returned types.EvalType should be one of: types.ETInt, types.ETDecimal, types.ETReal
-func numericContextResultType(ft *types.FieldType) types.EvalType {
+func numericContextResultType(expr Expression) types.EvalType {
+	ft := expr.GetType()
 	if types.IsTypeTemporal(ft.GetType()) {
 		if ft.GetDecimal() > 0 {
 			return types.ETDecimal
 		}
 		return types.ETInt
 	}
-	if types.IsBinaryStr(ft) || ft.GetType() == mysql.TypeBit {
+	// to solve https://github.com/pingcap/tidb/issues/27698
+	// if expression is constant binary literal, like `0x1234`, `0b00011`, cast to integer
+	// for other binary str column related expression, like varbinary, cast to float/double.
+	if isConstantBinaryLiteral(expr) || ft.GetType() == mysql.TypeBit {
 		return types.ETInt
 	}
 	evalTp4Ft := types.ETReal
@@ -153,8 +169,7 @@ func (c *arithmeticPlusFunctionClass) getFunction(ctx sessionctx.Context, args [
 	if err := c.verifyArgs(args); err != nil {
 		return nil, err
 	}
-	lhsTp, rhsTp := args[0].GetType(), args[1].GetType()
-	lhsEvalTp, rhsEvalTp := numericContextResultType(lhsTp), numericContextResultType(rhsTp)
+	lhsEvalTp, rhsEvalTp := numericContextResultType(args[0]), numericContextResultType(args[1])
 	if lhsEvalTp == types.ETReal || rhsEvalTp == types.ETReal {
 		bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETReal, types.ETReal, types.ETReal)
 		if err != nil {
@@ -305,8 +320,7 @@ func (c *arithmeticMinusFunctionClass) getFunction(ctx sessionctx.Context, args
 	if err := c.verifyArgs(args); err != nil {
 		return nil, err
 	}
-	lhsTp, rhsTp := args[0].GetType(), args[1].GetType()
-	lhsEvalTp, rhsEvalTp := numericContextResultType(lhsTp), numericContextResultType(rhsTp)
+	lhsEvalTp, rhsEvalTp := numericContextResultType(args[0]), numericContextResultType(args[1])
 	if lhsEvalTp == types.ETReal || rhsEvalTp == types.ETReal {
 		bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETReal, types.ETReal, types.ETReal)
 		if err != nil {
@@ -492,7 +506,7 @@ func (c *arithmeticMultiplyFunctionClass) getFunction(ctx sessionctx.Context, ar
 		return nil, err
 	}
 	lhsTp, rhsTp := args[0].GetType(), args[1].GetType()
-	lhsEvalTp, rhsEvalTp := numericContextResultType(lhsTp), numericContextResultType(rhsTp)
+	lhsEvalTp, rhsEvalTp := numericContextResultType(args[0]), numericContextResultType(args[1])
 	if lhsEvalTp == types.ETReal || rhsEvalTp == types.ETReal {
 		bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETReal, types.ETReal, types.ETReal)
 		if err != nil {
@@ -639,7 +653,7 @@ func (c *arithmeticDivideFunctionClass) getFunction(ctx sessionctx.Context, args
 		return nil, err
 	}
 	lhsTp, rhsTp := args[0].GetType(), args[1].GetType()
-	lhsEvalTp, rhsEvalTp := numericContextResultType(lhsTp), numericContextResultType(rhsTp)
+	lhsEvalTp, rhsEvalTp := numericContextResultType(args[0]), numericContextResultType(args[1])
 	if lhsEvalTp == types.ETReal || rhsEvalTp == types.ETReal {
 		bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETReal, types.ETReal, types.ETReal)
 		if err != nil {
@@ -732,9 +746,8 @@ func (c *arithmeticIntDivideFunctionClass) getFunction(ctx sessionctx.Context, a
 	if err := c.verifyArgs(args); err != nil {
 		return nil, err
 	}
-
 	lhsTp, rhsTp := args[0].GetType(), args[1].GetType()
-	lhsEvalTp, rhsEvalTp := numericContextResultType(lhsTp), numericContextResultType(rhsTp)
+	lhsEvalTp, rhsEvalTp := numericContextResultType(args[0]), numericContextResultType(args[1])
 	if lhsEvalTp == types.ETInt && rhsEvalTp == types.ETInt {
 		bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETInt, types.ETInt, types.ETInt)
 		if err != nil {
@@ -897,7 +910,7 @@ func (c *arithmeticModFunctionClass) getFunction(ctx sessionctx.Context, args []
 		return nil, err
 	}
 	lhsTp, rhsTp := args[0].GetType(), args[1].GetType()
-	lhsEvalTp, rhsEvalTp := numericContextResultType(lhsTp), numericContextResultType(rhsTp)
+	lhsEvalTp, rhsEvalTp := numericContextResultType(args[0]), numericContextResultType(args[1])
 	if lhsEvalTp == types.ETReal || rhsEvalTp == types.ETReal {
 		bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETReal, types.ETReal, types.ETReal)
 		if err != nil {


Test patch:
diff --git a/executor/executor_test.go b/executor/executor_test.go
index 2765af034e3d6..880bbd62a12c0 100644
--- a/executor/executor_test.go
+++ b/executor/executor_test.go
@@ -6056,3 +6056,26 @@ func TestIsFastPlan(t *testing.T) {
 		require.Equal(t, ca.isFastPlan, ok)
 	}
 }
+
+func TestBinaryStrNumericOperator(t *testing.T) {
+	store, clean := testkit.CreateMockStore(t)
+	defer clean()
+
+	tk := testkit.NewTestKit(t, store)
+	tk.MustExec("use test")
+
+	// Test normal warnings.
+	tk.MustExec("drop table if exists t")
+	tk.MustExec("create table t(a varbinary(10))")
+	tk.MustExec("insert into t values ('123.12')")
+	tk.MustQuery("select 1+a from t").Check(testkit.Rows(
+		"124.12"))
+	tk.MustQuery("select a-1 from t").Check(testkit.Rows(
+		"122.12"))
+	tk.MustQuery("select -10*a from t").Check(testkit.Rows(
+		"-1231.2"))
+	tk.MustQuery("select a/-2 from t").Check(testkit.Rows(
+		"-61.56"))
+	// there should be no warning.
+	tk.MustQuery("show warnings").Check(testkit.Rows())
+}