Skip to content

Commit 9b9b92a

Browse files
authored
fix(nodes,scalar): fix EXPLAIN crash and wrong scan range with CASE WHEN subquery (#35118)
1 parent b03d7e1 commit 9b9b92a

5 files changed

Lines changed: 178 additions & 2 deletions

File tree

source/libs/nodes/src/nodesToSQLFuncs.c

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,9 @@ int32_t nodesNodeToSQLFormat(SNode *pNode, char *buf, int32_t bufSize, int32_t *
135135
SValueNode *colNode = (SValueNode *)pNode;
136136
char *t = nodesGetStrValueFromNode(colNode);
137137
if (NULL == t) {
138-
nodesError("fail to get str value from valueNode");
139-
NODES_ERR_RET(TSDB_CODE_APP_ERROR);
138+
// NULL type value (e.g. ELSE NULL in CASE WHEN)
139+
*len += tsnprintf(buf + *len, bufSize - *len, "NULL");
140+
return TSDB_CODE_SUCCESS;
140141
}
141142

142143
int32_t tlen = strlen(t);
@@ -235,6 +236,33 @@ int32_t nodesNodeToSQLFormat(SNode *pNode, char *buf, int32_t bufSize, int32_t *
235236

236237
return TSDB_CODE_SUCCESS;
237238
}
239+
case QUERY_NODE_WHEN_THEN: {
240+
SWhenThenNode *pWhenThen = (SWhenThenNode *)pNode;
241+
*len += tsnprintf(buf + *len, bufSize - *len, "WHEN ");
242+
NODES_ERR_RET(nodesNodeToSQLFormat(pWhenThen->pWhen, buf, bufSize, len, true));
243+
*len += tsnprintf(buf + *len, bufSize - *len, " THEN ");
244+
NODES_ERR_RET(nodesNodeToSQLFormat(pWhenThen->pThen, buf, bufSize, len, true));
245+
return TSDB_CODE_SUCCESS;
246+
}
247+
case QUERY_NODE_CASE_WHEN: {
248+
SCaseWhenNode *pCaseWhen = (SCaseWhenNode *)pNode;
249+
*len += tsnprintf(buf + *len, bufSize - *len, "CASE");
250+
if (pCaseWhen->pCase) {
251+
*len += tsnprintf(buf + *len, bufSize - *len, " ");
252+
NODES_ERR_RET(nodesNodeToSQLFormat(pCaseWhen->pCase, buf, bufSize, len, true));
253+
}
254+
SNode *pWhenThen = NULL;
255+
FOREACH(pWhenThen, pCaseWhen->pWhenThenList) {
256+
*len += tsnprintf(buf + *len, bufSize - *len, " ");
257+
NODES_ERR_RET(nodesNodeToSQLFormat(pWhenThen, buf, bufSize, len, true));
258+
}
259+
if (pCaseWhen->pElse) {
260+
*len += tsnprintf(buf + *len, bufSize - *len, " ELSE ");
261+
NODES_ERR_RET(nodesNodeToSQLFormat(pCaseWhen->pElse, buf, bufSize, len, true));
262+
}
263+
*len += tsnprintf(buf + *len, bufSize - *len, " END");
264+
return TSDB_CODE_SUCCESS;
265+
}
238266
default:
239267
break;
240268
}

source/libs/nodes/src/nodesUtilFuncs.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2569,6 +2569,9 @@ char* nodesGetStrValueFromNode(SValueNode* pNode) {
25692569
case TSDB_DATA_TYPE_VARCHAR:
25702570
case TSDB_DATA_TYPE_VARBINARY:
25712571
case TSDB_DATA_TYPE_GEOMETRY: {
2572+
if (NULL == pNode->datum.p) {
2573+
return NULL;
2574+
}
25722575
int32_t bufSize = varDataLen(pNode->datum.p) + 2 + 1;
25732576
void* buf = taosMemoryMalloc(bufSize);
25742577
if (NULL == buf) {

source/libs/scalar/src/filter.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5629,6 +5629,13 @@ static EDealRes classifyConditionImpl(SNode *pNode, void *pContext) {
56295629
} else {
56305630
pCxt->hasOtherCol = true;
56315631
}
5632+
} else if (QUERY_NODE_CASE_WHEN == nodeType(pNode)) {
5633+
// CASE WHEN expressions are computed values, not direct primary key comparisons.
5634+
// Traversing into them could incorrectly classify the expression as COND_TYPE_PRIMARY_KEY
5635+
// when the CASE body references ts, causing filterGetTimeRange to receive an unextractable
5636+
// condition and fall back to TSWINDOW_INITIALIZER (full scan).
5637+
pCxt->hasOtherCol = true;
5638+
return DEAL_RES_IGNORE_CHILD;
56325639
} else if (QUERY_NODE_FUNCTION == nodeType(pNode)) {
56335640
SFunctionNode *pFunc = (SFunctionNode *)pNode;
56345641
if (fmIsPseudoColumnFunc(pFunc->funcId)) {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from new_test_framework.utils import tdLog, tdSql, sc, clusterComCheck, tdCom
2+
3+
4+
class TestExplainCaseWhen:
5+
6+
def setup_class(cls):
7+
tdLog.debug(f"start to execute {__file__}")
8+
9+
def test_explain_case_when_scan_range(self):
10+
"""explain verbose true with CASE WHEN subquery
11+
12+
1. EXPLAIN VERBOSE TRUE on a query that uses CASE WHEN inside a subquery
13+
with the outer WHERE filtering on the computed CASE column should not crash.
14+
2. The scan time range reported by EXPLAIN VERBOSE TRUE must match the
15+
explicit ts filters in the inner WHERE clause, not the full history range.
16+
17+
Catalog:
18+
- Query:Explain
19+
20+
Since: v3.3.6.0
21+
22+
Labels: common,ci
23+
24+
Jira: TD-34178
25+
26+
History:
27+
- 2026-04-13 Wei Pan Created to cover two related bugs:
28+
(a) EXPLAIN VERBOSE TRUE crashes with "unknown node = CaseWhen"
29+
(b) Server scans wrong time range when outer WHERE filters on a CASE WHEN alias
30+
31+
"""
32+
33+
# ------------------------------------------------------------------
34+
# Setup
35+
# ------------------------------------------------------------------
36+
tdSql.execute("DROP DATABASE IF EXISTS test_case_when_explain")
37+
tdSql.execute("CREATE DATABASE test_case_when_explain PRECISION 'ms'")
38+
tdSql.execute("USE test_case_when_explain")
39+
tdSql.execute(
40+
"CREATE STABLE device_data "
41+
"(ts TIMESTAMP, val DOUBLE, zone_id NCHAR(32)) "
42+
"TAGS (device_sn NCHAR(64))"
43+
)
44+
tdSql.execute(
45+
"CREATE TABLE device_001 USING device_data TAGS ('DEV001')"
46+
)
47+
# Insert rows: one before, two inside, one after the test window
48+
tdSql.execute("INSERT INTO device_001 VALUES ('2020-12-31 23:59:59.000', 0.0, 'UTC')")
49+
tdSql.execute("INSERT INTO device_001 VALUES ('2021-01-01 06:00:00.000', 1.0, 'UTC')")
50+
tdSql.execute("INSERT INTO device_001 VALUES ('2021-01-01 18:00:00.000', 2.0, 'UTC')")
51+
tdSql.execute("INSERT INTO device_001 VALUES ('2021-01-02 00:00:01.000', 3.0, 'UTC')")
52+
53+
# ------------------------------------------------------------------
54+
# The query under test – DO NOT change this SQL; it is the exact
55+
# pattern reported in TD-34178.
56+
# ------------------------------------------------------------------
57+
# Window: [2021-01-01 00:00:00.000, 2021-01-02 00:00:00.000)
58+
# Expected time range is derived from the server's timezone to avoid
59+
# hardcoding UTC offsets that differ across environments.
60+
inner_ts_start = "2021-01-01 00:00:00.000"
61+
inner_ts_end = "2021-01-02 00:00:00.000"
62+
63+
# Query the server to get the actual epoch-ms for these timestamp strings
64+
tdSql.query(
65+
f"SELECT to_unixtimestamp('{inner_ts_start}'), to_unixtimestamp('{inner_ts_end}') "
66+
f"FROM device_001 LIMIT 1"
67+
)
68+
expected_skey = tdSql.queryResult[0][0]
69+
expected_ekey = tdSql.queryResult[0][1] - 1 # exclusive → inclusive
70+
71+
test_sql = f"""
72+
SELECT
73+
time_period,
74+
first(val) AS first_val,
75+
last(val) AS last_val,
76+
first(zone_id) AS zone_id
77+
FROM (
78+
SELECT
79+
CASE
80+
WHEN ts >= '{inner_ts_start}' AND ts < '{inner_ts_end}'
81+
THEN 'PERIOD_20210101'
82+
ELSE NULL
83+
END AS time_period,
84+
val,
85+
zone_id,
86+
ts
87+
FROM device_001
88+
WHERE ts >= '{inner_ts_start}'
89+
AND ts < '{inner_ts_end}'
90+
) t
91+
WHERE time_period IS NOT NULL
92+
GROUP BY time_period
93+
ORDER BY time_period
94+
"""
95+
96+
# ------------------------------------------------------------------
97+
# (a) Verify EXPLAIN VERBOSE TRUE does not raise an error
98+
# ------------------------------------------------------------------
99+
tdLog.info("step1: EXPLAIN VERBOSE TRUE must not crash on CASE WHEN subquery")
100+
tdSql.query(f"EXPLAIN VERBOSE TRUE {test_sql}")
101+
102+
# ------------------------------------------------------------------
103+
# (b) Verify the scan time range matches the inner WHERE conditions
104+
# ------------------------------------------------------------------
105+
tdLog.info("step2: check scan Time Range in EXPLAIN output")
106+
time_range_line = f"Time Range: [{expected_skey}, {expected_ekey}]"
107+
found = False
108+
for row in tdSql.queryResult:
109+
# Each row is a one-column tuple containing the explain text
110+
if time_range_line in str(row[0]):
111+
found = True
112+
break
113+
if not found:
114+
tdLog.info(f"EXPLAIN output rows:")
115+
for row in tdSql.queryResult:
116+
tdLog.info(f" {row[0]}")
117+
assert found, (
118+
f"Expected '{time_range_line}' in EXPLAIN VERBOSE TRUE output, "
119+
f"but it was not found. The server may be scanning the wrong time range."
120+
)
121+
122+
# ------------------------------------------------------------------
123+
# (c) EXPLAIN ANALYZE VERBOSE TRUE must also not crash
124+
# ------------------------------------------------------------------
125+
tdLog.info("step3: EXPLAIN ANALYZE VERBOSE TRUE must not crash")
126+
tdSql.query(f"EXPLAIN ANALYZE VERBOSE TRUE {test_sql}")
127+
128+
# ------------------------------------------------------------------
129+
# (d) Functional correctness: only the two rows inside the window
130+
# should be returned.
131+
# ------------------------------------------------------------------
132+
tdLog.info("step4: query correctness – only rows inside window returned")
133+
tdSql.query(test_sql)
134+
tdSql.checkRows(1)
135+
tdSql.checkData(0, 0, "PERIOD_20210101")
136+
tdSql.checkData(0, 1, 1.0) # first(val)
137+
tdSql.checkData(0, 2, 2.0) # last(val)

test/ci/cases.task

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@
353353
## 14-Explain
354354
,,y,.,./ci/pytest.sh pytest cases/07-DataQuerying/14-Explain/test_explain.py
355355
,,y,.,./ci/pytest.sh pytest cases/07-DataQuerying/14-Explain/test_explain_tsorder.py
356+
,,y,.,./ci/pytest.sh pytest cases/07-DataQuerying/14-Explain/test_explain_case_when.py
356357
## 15-Hints
357358
## 16-Psedocolumn
358359
,,y,.,./ci/pytest.sh pytest cases/07-DataQuerying/16-PsedoColumn/test_tbname_in.py

0 commit comments

Comments
 (0)