pingcap / pingcap/tidb

TiKV Coprocessor Ignores `NO_ZERO_DATE`/`NO_ZERO_IN_DATE` SQL Mode During Invalid Date/Time Cast, Causing Inflated `COUNT()`

Open
#69,292 2 comments 0 reactions 0 assignees View on GitHub
component/tikv contribution may-affects-7.5 may-affects-8.1 may-affects-8.5 severity/critical type/bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.