TiKV Coprocessor Ignores `NO_ZERO_DATE`/`NO_ZERO_IN_DATE` SQL Mode During Invalid Date/Time Cast, Causing Inflated `COUNT()`
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
## Bug Report
Please answer these questions before submitting your issue. Thanks!
### 1. Minimal reproduce step (Required)
### Case 1: `DECIMAL` → `DATE` → `DATETIME`
```sql
DROP DATABASE IF EXISTS repro_tidb615_db12_min;
CREATE DATABASE repro_tidb615_db12_min;
USE repro_tidb615_db12_min;
SET SESSION sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
SET time_zone='+00:00';
CREATE TABLE src(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
c0 DECIMAL(65,30)
);
INSERT INTO src(c0) VALUES (0),(1);
CREATE TABLE l(
id BIGINT NOT NULL PRIMARY KEY,
c0 DECIMAL(65,30)
);
CREATE TABLE r(
id BIGINT NOT NULL PRIMARY KEY
);
INSERT INTO l SELECT id, c0 FROM src;
INSERT INTO r SELECT id FROM src;
-- Single‑table: returns 2 (incorrect)
SELECT COUNT(
CAST(
CASE 'x'
WHEN id >= id THEN c0
ELSE CAST(FALSE AS DATE)
END AS DATETIME
)
) AS single_count
FROM src;
-- Split/rewrite via join: returns 0 (correct)
SELECT COUNT(
CAST(
CASE 'x'
WHEN v.id >= v.id THEN v.c0
ELSE CAST(FALSE AS DATE)
END AS DATETIME
)
) AS split_count
FROM (
SELECT l.id, l.c0
FROM l JOIN r ON l.id = r.id
) AS v;
```
### Case 2: `DOUBLE` → `DATETIME` → `DATE`
```sql
DROP DATABASE IF EXISTS repro_tidb615_db3_min;
CREATE DATABASE repro_tidb615_db3_min;
USE repro_tidb615_db3_min;
SET SESSION sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
SET time_zone='+00:00';
CREATE TABLE src(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
c0 DOUBLE
);
INSERT INTO src(c0) VALUES (0),(1),(2);
CREATE TABLE l(
id BIGINT NOT NULL PRIMARY KEY,
c0 DOUBLE
);
CREATE TABLE r(
id BIGINT NOT NULL PRIMARY KEY
);
INSERT INTO l SELECT id, c0 FROM src;
INSERT INTO r SELECT id FROM src;
-- Single‑table: returns 1 (incorrect)
SELECT COUNT(
CAST(
CASE (id IS NULL)
WHEN id THEN 1
WHEN id THEN 'x'
ELSE CAST(c0 AS DATETIME)
END AS DATE
)
) AS single_count
FROM src;
-- Split/rewrite via IN subquery: returns 0 (correct)
SELECT COUNT(
CAST(
CASE (v.id IS NULL)
WHEN v.id THEN 1
WHEN v.id THEN 'x'
ELSE CAST(v.c0 AS DATETIME)
END AS DATE
)
) AS split_count
FROM (
SELECT l.id, l.c0
FROM l
WHERE l.id IN (SELECT r.id FROM r)
) AS v;
```
### 2. What did you expect to see? (Required)
**Case 1:** The `CASE` expression always falls through to the `ELSE` branch (`CAST(FALSE AS DATE)`), which produces `'0000-00-00'` — an illegal date under the `NO_ZERO_DATE` SQL mode. The outer `CAST(... AS DATETIME)` should therefore evaluate to `NULL`, and `COUNT()` should return `0`.
**Case 2:** The `CASE` expression always reaches the `ELSE` branch (`CAST(c0 AS DATETIME)`) with `c0` values `0`, `1`, `2`. In `NO_ZERO_DATE` mode, casting `0`, `1`, `2` to `DATETIME` yields `'0000-00-00'`, `'0000-00-01'`, `'0000-00-02'`, all of which are invalid zero dates. The subsequent cast to `DATE` should produce `NULL`. `COUNT()` should return `0`.
Both single‑table and join/subquery queries should return `0` in each case.
### 3. What did you see instead (Required)
| Case | Single‑table result | Split/join result |
|------|---------------------|--------------------|
| Case 1 | `2` (wrong) | `0` (correct) |
| Case 2 | `1` (wrong) | `0` (correct) |
The single‑table queries count rows that should have been `NULL` after casting invalid date/time values, whereas the equivalent queries using a join or subquery correctly return `0`.
### 4. What is your TiDB version? (Required)
I tested such a case in TiDB-v9.0.0.
## 5. Execution Plan Differences
**Single‑table queries (wrong)** — Aggregation entirely pushed down to TiKV (`cop[tikv]`):
Case 1:
```
HashAgg_5 [cop[tikv]] funcs:count(cast(case(...), datetime BINARY))->Column#5
```
Case 2:
```
HashAgg_5 [cop[tikv]] funcs:count(cast(case(... cast(cast(src.c0, datetime), var_string) ...), date BINARY))->Column#5
```
In both cases the `COUNT(CAST(...))` expression is evaluated inside the TiKV coprocessor, and the aggregation result is sent back to TiDB root.
**Join/subquery queries (correct)** — Expression evaluated at TiDB root, aggregation at root:
Case 1:
```
Projection_58 [root] cast(case(...), datetime BINARY)->Column#7
HashAgg_13 [root] funcs:count(Column#7)->Column#6
```
Case 2:
```
Projection_64 [root] cast(case(... cast(cast(l.c0, datetime), var_string) ...), date BINARY)->Column#7
HashAgg_17 [root] funcs:count(Column#7)->Column#6
```
Here the complex cast expression is evaluated in the TiDB root layer before the `COUNT` aggregate is applied.
## 6. Root Cause
The discrepancy is caused by an inconsistency between the TiKV coprocessor and the TiDB root executor in handling invalid date/time casts. When the aggregate is pushed to TiKV, the stricter SQL‑mode rules are bypassed, leading to an inflated `COUNT()` value.
Contributor guide
Assessment
This issue has not been assessed yet.