Skip to content

Commit 26a512c

Browse files
kyleconroyclaude
andcommitted
Handle all-NULL tuple literals in IN expression EXPLAIN output
When an IN expression contains only NULL literals (e.g., `IN (NULL, NULL)`), format them as a tuple literal `Tuple_(NULL, NULL)` instead of `Function tuple`. This matches ClickHouse's EXPLAIN AST output. The fix adds an `allNull` flag to track when all items are NULL and allows tuple literal formatting in this case. Applied to both explainInExpr and explainInExprWithAlias functions. Fixes 01558_transform_null_in/stmt21 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3956e3f commit 26a512c

2 files changed

Lines changed: 14 additions & 10 deletions

File tree

internal/explain/functions.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,13 +1034,15 @@ func explainInExpr(sb *strings.Builder, n *ast.InExpr, indent string, depth int)
10341034
allTuples := true
10351035
allTuplesArePrimitive := true
10361036
allPrimitiveLiterals := true // New: check if all are primitive literals (any type)
1037+
allNull := true // Track if all items are NULL
10371038
hasNonNull := false // Need at least one non-null value
10381039
for _, item := range n.List {
10391040
if lit, ok := item.(*ast.Literal); ok {
10401041
if lit.Type == ast.LiteralNull {
10411042
// NULL is compatible with all literal type lists
10421043
continue
10431044
}
1045+
allNull = false
10441046
hasNonNull = true
10451047
if lit.Type != ast.LiteralInteger && lit.Type != ast.LiteralFloat {
10461048
allNumericOrNull = false
@@ -1066,12 +1068,14 @@ func explainInExpr(sb *strings.Builder, n *ast.InExpr, indent string, depth int)
10661068
}
10671069
} else if isNumericExpr(item) {
10681070
// Unary minus of numeric is still numeric
1071+
allNull = false
10691072
hasNonNull = true
10701073
allStringsOrNull = false
10711074
allBooleansOrNull = false
10721075
allTuples = false
10731076
// Numeric expression counts as primitive
10741077
} else {
1078+
allNull = false
10751079
allNumericOrNull = false
10761080
allStringsOrNull = false
10771081
allBooleansOrNull = false
@@ -1082,7 +1086,8 @@ func explainInExpr(sb *strings.Builder, n *ast.InExpr, indent string, depth int)
10821086
}
10831087
// Allow combining mixed primitive literals into a tuple when comparing tuples
10841088
// This handles cases like: (1,'') IN (-1,'') where the right side should be a single tuple literal
1085-
canBeTupleLiteral = hasNonNull && (allNumericOrNull || allStringsOrNull || allBooleansOrNull || (allTuples && allTuplesArePrimitive) || allPrimitiveLiterals)
1089+
// Also allow all-NULL lists to be formatted as tuple literals
1090+
canBeTupleLiteral = allNull || (hasNonNull && (allNumericOrNull || allStringsOrNull || allBooleansOrNull || (allTuples && allTuplesArePrimitive) || allPrimitiveLiterals))
10861091
}
10871092

10881093
// Count arguments: expr + list items or subquery
@@ -1252,13 +1257,15 @@ func explainInExprWithAlias(sb *strings.Builder, n *ast.InExpr, alias string, in
12521257
allTuples := true
12531258
allTuplesArePrimitive := true
12541259
allPrimitiveLiterals := true // Any mix of primitive literals (numbers, strings, booleans, null, primitive tuples)
1260+
allNull := true // Track if all items are NULL
12551261
hasNonNull := false // Need at least one non-null value
12561262
for _, item := range n.List {
12571263
if lit, ok := item.(*ast.Literal); ok {
12581264
if lit.Type == ast.LiteralNull {
12591265
// NULL is compatible with all literal type lists
12601266
continue
12611267
}
1268+
allNull = false
12621269
hasNonNull = true
12631270
if lit.Type != ast.LiteralInteger && lit.Type != ast.LiteralFloat {
12641271
allNumericOrNull = false
@@ -1278,11 +1285,13 @@ func explainInExprWithAlias(sb *strings.Builder, n *ast.InExpr, alias string, in
12781285
}
12791286
}
12801287
} else if isNumericExpr(item) {
1288+
allNull = false
12811289
hasNonNull = true
12821290
allStringsOrNull = false
12831291
allBooleansOrNull = false
12841292
allTuples = false
12851293
} else {
1294+
allNull = false
12861295
allNumericOrNull = false
12871296
allStringsOrNull = false
12881297
allBooleansOrNull = false
@@ -1291,7 +1300,7 @@ func explainInExprWithAlias(sb *strings.Builder, n *ast.InExpr, alias string, in
12911300
break
12921301
}
12931302
}
1294-
canBeTupleLiteral = hasNonNull && (allNumericOrNull || (allStringsOrNull && len(n.List) <= maxStringTupleSizeWithAlias) || allBooleansOrNull || (allTuples && allTuplesArePrimitive) || allPrimitiveLiterals)
1303+
canBeTupleLiteral = allNull || (hasNonNull && (allNumericOrNull || (allStringsOrNull && len(n.List) <= maxStringTupleSizeWithAlias) || allBooleansOrNull || (allTuples && allTuplesArePrimitive) || allPrimitiveLiterals))
12951304
}
12961305

12971306
// Count arguments
@@ -1342,9 +1351,8 @@ func explainInExprWithAlias(sb *strings.Builder, n *ast.InExpr, alias string, in
13421351
fmt.Fprintf(sb, "%s Literal %s\n", indent, FormatLiteral(tupleLit))
13431352
} else if len(n.List) == 1 {
13441353
if lit, ok := n.List[0].(*ast.Literal); ok && lit.Type == ast.LiteralTuple {
1345-
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
1346-
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1)
1347-
Node(sb, n.List[0], depth+4)
1354+
// Use explainTupleInInList to properly handle primitive-only tuples as Literal Tuple_
1355+
explainTupleInInList(sb, lit, indent+" ", depth+2)
13481356
} else if n.TrailingComma {
13491357
// Single element with trailing comma (e.g., (2,)) - wrap in Function tuple
13501358
fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1)
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt21": true
4-
}
5-
}
1+
{}

0 commit comments

Comments
 (0)