Repository: pingcap/tidb (Go)

Issue:
`cast as char` is not compatible with MySQL
## Bug Report

Please answer these questions before submitting your issue. Thanks!

1. What did you do?
If possible, provide a recipe for reproducing the error.
```
create table t(a year);
insert into t values(0);
select *, cast('0000' as char), cast(a as char) from t;
```

2. What did you expect to see?
```
mysql> create table t(a year);
Query OK, 0 rows affected (0.07 sec)

mysql> insert into t values(0);
Query OK, 1 row affected (0.01 sec)

mysql> select *, cast('0000' as char), cast(a as char) from t;
+------+----------------------+-----------------+
| a    | cast('0000' as char) | cast(a as char) |
+------+----------------------+-----------------+
| 0000 | 0000                 | 0000            |
+------+----------------------+-----------------+
1 row in set (0.00 sec)
```

3. What did you see instead?
```
mysql> create table t(a year);
Query OK, 0 rows affected (0.01 sec)
mysql> insert into t values(0);
Query OK, 1 row affected (0.00 sec)

mysql> select *, cast('0000' as char), cast(a as char) from t;
+------+----------------------+-----------------+
| a    | cast('0000' as char) | cast(a as char) |
+------+----------------------+-----------------+
| 0000 | 0000                 | 0               |
+------+----------------------+-----------------+
1 row in set (0.00 sec)
```

4. What version of TiDB are you using (`tidb-server -V` or run `select tidb_version();` on TiDB)?
```
mysql> select tidb_version();
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| tidb_version()                                                                                                                                                                                                                                                                                                                        |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Release Version: v3.0.0-beta-260-g47a564b-dirty
Git Commit Hash: 47a564be2fc6045c6423e63b28a421924d5557bf
Git Branch: master
UTC Build Time: 2019-03-26 07:16:37
GoVersion: go version go1.12 linux/amd64
Race Enabled: false
TiKV Min Version: 2.1.0-alpha.1-ff3dd160846b7d1aed9079c389fc188f7f5ea13e
Check Table Before Drop: false |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```



Discussion:
Thanks for your feedback, @wjhuang2016 !
We have a plan to improve the compatibility of `cast` in the future.
btw: It seems that you're an expert of the `cast` behavior of MySQL. Any suggestions for us? Is there a cast test suite for MySQL?
@jackysp Thanks.
I am far from being an expert of the cast behavior of MySQL. All I did was search the bug list in MySQL to see if TiDB had this bug as well, and I don't think the type system in TiDB is mature enough.
There is a cast test suite for MySQL, it's in mysql-test/t/cast.test, I think it's useful for TiDB.
Confirming I can still reproduce this against master:
```sql
DROP TABLE IF EXISTS t;
create table t(a year);
insert into t values(0);
select *, cast('0000' as char), cast(a as char) from t;

..
mysql> select *, cast('0000' as char), cast(a as char) from t;
+------+----------------------+-----------------+
| a    | cast('0000' as char) | cast(a as char) |
+------+----------------------+-----------------+
| 0000 | 0000                 | 0               |
+------+----------------------+-----------------+
1 row in set (0.00 sec)

mysql> SELECT tidb_version()\G
*************************** 1. row ***************************
tidb_version(): Release Version: v4.0.0-beta.2-821-gfe07417e6
Edition: Community
Git Commit Hash: fe07417e6b2f28eb334117d44640d0760d7e9866
Git Branch: master
UTC Build Time: 2020-07-22 12:51:37
GoVersion: go1.13
Race Enabled: false
TiKV Min Version: v3.0.0-60965b006877ca7234adaced7890d7b029ed1306
Check Table Before Drop: false
1 row in set (0.00 sec)
```
/assign @ichn-hu 
## Please edit this comment or add a new comment to complete the following information

### Not a bug

1. Remove the 'type/bug' label
2. Add notes to indicate why it is not a bug

### Duplicate bug

1. Add the 'type/duplicate' label
2. Add the link to the original bug

### Bug

Note: Make Sure that 'component', and 'severity' labels are added
Example for how to fill out the template: https://github.com/pingcap/tidb/issues/20100

#### 1. Root Cause Analysis (RCA) (optional) 
<!-- Write down the reason why this bug occurs -->

#### 2. Symptom (optional)

<!-- What will the user see when this bug occurs. The error message may be in the terminal, log or monitoring -->

#### 3. All Trigger Conditions (optional)

<!-- All the user scenarios that may trigger this bug -->

#### 4. Workaround (optional)

#### 5. Affected versions

<!--
In the format of [start_version:end_version], multiple version ranges are
accepted. If the bug only affects the unreleased version, please input:
"unreleased". For example:

Notes:
  1. Do not use any white spaces in '[]'.
  2. The range in '[]' is a closed interval
  3. The version format is `v$Major.$Minor.$Patch`, the $Majoy and $Minor
     number in a version range should be the same. [v3.0.1:v3.1.2] is
     invalid because the $Minor number of the version range is different.

Example 1: [v3.0.1:v3.0.5], [v4.0.1:v4.0.5]
Example 2: unreleased
-->

#### 6. Fixed versions

<!--
The first released version that contains this fix in each minor version. If the bug's affected version has been released, the fixed version should be a detailed version number; If the bug doesn't affect any released version, the fixed version can be "master";  

Example 1: v3.0.13, v4.0.5
Example 2: master
-->



Patch:
diff --git a/expression/builtin_cast.go b/expression/builtin_cast.go
index 4f16aa60db002..d64e83928b1e6 100644
--- a/expression/builtin_cast.go
+++ b/expression/builtin_cast.go
@@ -546,11 +546,15 @@ func (b *builtinCastIntAsStringSig) evalString(row chunk.Row) (res string, isNul
 	if isNull || err != nil {
 		return res, isNull, err
 	}
-	if !mysql.HasUnsignedFlag(b.args[0].GetType().Flag) {
+	tp := b.args[0].GetType()
+	if !mysql.HasUnsignedFlag(tp.Flag) {
 		res = strconv.FormatInt(val, 10)
 	} else {
 		res = strconv.FormatUint(uint64(val), 10)
 	}
+	if tp.Tp == mysql.TypeYear && res == "0" {
+		res = "0000"
+	}
 	res, err = types.ProduceStrWithSpecifiedTp(res, b.tp, b.ctx.GetSessionVars().StmtCtx, false)
 	if err != nil {
 		return res, false, err
diff --git a/expression/builtin_cast_vec.go b/expression/builtin_cast_vec.go
index 47214e5a0f5af..f42eb9f1cc58e 100644
--- a/expression/builtin_cast_vec.go
+++ b/expression/builtin_cast_vec.go
@@ -636,7 +636,9 @@ func (b *builtinCastIntAsStringSig) vecEvalString(input *chunk.Chunk, result *ch
 		return err
 	}
 
-	isUnsigned := mysql.HasUnsignedFlag(b.args[0].GetType().Flag)
+	tp := b.args[0].GetType()
+	isUnsigned := mysql.HasUnsignedFlag(tp.Flag)
+	isYearType := tp.Tp == mysql.TypeYear
 	result.ReserveString(n)
 	i64s := buf.Int64s()
 	for i := 0; i < n; i++ {
@@ -650,6 +652,9 @@ func (b *builtinCastIntAsStringSig) vecEvalString(input *chunk.Chunk, result *ch
 		} else {
 			str = strconv.FormatUint(uint64(i64s[i]), 10)
 		}
+		if isYearType && str == "0" {
+			str = "0000"
+		}
 		str, err = types.ProduceStrWithSpecifiedTp(str, b.tp, b.ctx.GetSessionVars().StmtCtx, false)
 		if err != nil {
 			return err


Test patch:
diff --git a/executor/executor_test.go b/executor/executor_test.go
index 2be54177cc9a2..d18b47ab9dc14 100644
--- a/executor/executor_test.go
+++ b/executor/executor_test.go
@@ -7119,6 +7119,15 @@ func (s *testSuite) Test17780(c *C) {
 	tk.MustQuery("select count(*) from t0 where c0 = 0").Check(testkit.Rows("0"))
 }
 
+func (s *testSuite) TestIssue9918(c *C) {
+	tk := testkit.NewTestKit(c, s.store)
+	tk.MustExec("use test")
+	tk.MustExec("drop table if exists t")
+	tk.MustExec("create table t (a year)")
+	tk.MustExec("insert into t values(0)")
+	tk.MustQuery("select cast(a as char) from t").Check(testkit.Rows("0000"))
+}
+
 func (s *testSuite) Test13004(c *C) {
 	tk := testkit.NewTestKit(c, s.store)
 	// see https://dev.mysql.com/doc/refman/5.6/en/date-and-time-literals.html, timestamp here actually produces a datetime