Skip to content

Commit 3022bf6

Browse files
bluestreak01claude
andcommitted
Split mixed const/runtime AND-terms in constWhereClause
mergeConstIntoPostJoinWhereClause now flattens the AND-tree and classifies each conjunct independently. Compile-time constant terms (e.g. 1 > 10) stay in constWhereClause for EmptyTableRecordCursorFactory folding; runtime terms (e.g. NOW() = NOW()) move to postJoinWhereClause. Previously, a mixed expression like (1 > 10 AND NOW() = NOW()) was treated as a single non-compile-time block, losing the Empty table optimization. Adds extractAndTerms() and isCompileTimeConstant() static helpers. Adds tests for mixed false+runtime, true+runtime, and plan assertions. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent e38830c commit 3022bf6

File tree

3 files changed

+121
-15
lines changed

3 files changed

+121
-15
lines changed

core/src/main/java/io/questdb/griffin/SqlOptimiser.java

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,30 @@ private static ExpressionNode concatFilters(
407407
}
408408
}
409409

410+
private static void extractAndTerms(ExpressionNode node, ObjList<ExpressionNode> terms) {
411+
if (node.type == ExpressionNode.OPERATION && SqlKeywords.isAndKeyword(node.token)) {
412+
extractAndTerms(node.lhs, terms);
413+
extractAndTerms(node.rhs, terms);
414+
} else {
415+
terms.add(node);
416+
}
417+
}
418+
419+
// Returns true when every leaf in the expression tree is a literal constant
420+
// (no function calls, bind variables, or column references). Used to decide
421+
// whether a constWhereClause can be evaluated at compile time by the code
422+
// generator (e.g. folded to EmptyTableRecordCursorFactory when false).
423+
private static boolean isCompileTimeConstant(ExpressionNode node) {
424+
if (node == null) {
425+
return true;
426+
}
427+
return switch (node.type) {
428+
case ExpressionNode.CONSTANT -> true;
429+
case ExpressionNode.OPERATION -> isCompileTimeConstant(node.lhs) && isCompileTimeConstant(node.rhs);
430+
default -> false;
431+
};
432+
}
433+
410434
private static boolean isOrderedByDesignatedTimestamp(QueryModel model) {
411435
return model.getTimestamp() != null
412436
&& model.getOrderBy().size() == 1
@@ -1666,6 +1690,7 @@ private QueryModel bubbleUpOrderByAndLimitFromUnion(QueryModel model) throws Sql
16661690

16671691
// pushing predicates to sample by model is only allowed for sample by fill none align to calendar and expressions on non-timestamp columns
16681692
// pushing for other fill options or sample by first observation could alter a result
1693+
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
16691694
private boolean canPushToSampleBy(final QueryModel model, ObjList<CharSequence> expressionColumns) {
16701695
ObjList<ExpressionNode> fill = model.getSampleByFill();
16711696
int fillCount = fill.size();
@@ -4525,31 +4550,37 @@ private void mergeWindowSpec(WindowExpression child, WindowExpression base) {
45254550

45264551
private void mergeConstIntoPostJoinWhereClause(QueryModel model) {
45274552
ExpressionNode constWhere = model.getConstWhereClause();
4528-
if (constWhere != null && !isCompileTimeConstant(constWhere)) {
4553+
if (constWhere == null) {
4554+
return;
4555+
}
4556+
boolean legacy = configuration.getCairoSqlLegacyOperatorPrecedence();
4557+
ExpressionNode compileTimeTerms = null;
4558+
ExpressionNode runtimeTerms = null;
4559+
// Flatten the AND-tree and classify each conjunct.
4560+
tempExprs.clear();
4561+
extractAndTerms(constWhere, tempExprs);
4562+
for (int i = 0, n = tempExprs.size(); i < n; i++) {
4563+
ExpressionNode term = tempExprs.getQuick(i);
4564+
if (isCompileTimeConstant(term)) {
4565+
compileTimeTerms = concatFilters(legacy, expressionNodePool, compileTimeTerms, term);
4566+
} else {
4567+
runtimeTerms = concatFilters(legacy, expressionNodePool, runtimeTerms, term);
4568+
}
4569+
}
4570+
model.setConstWhereClause(compileTimeTerms);
4571+
if (runtimeTerms != null) {
45294572
IntList ordered = model.getOrderedJoinModels();
45304573
int lastIndex = ordered.getQuick(ordered.size() - 1);
45314574
QueryModel lastModel = model.getJoinModels().getQuick(lastIndex);
45324575
lastModel.setPostJoinWhereClause(concatFilters(
4533-
configuration.getCairoSqlLegacyOperatorPrecedence(),
4576+
legacy,
45344577
expressionNodePool,
45354578
lastModel.getPostJoinWhereClause(),
4536-
constWhere
4579+
runtimeTerms
45374580
));
4538-
model.setConstWhereClause(null);
45394581
}
45404582
}
45414583

4542-
private static boolean isCompileTimeConstant(ExpressionNode node) {
4543-
if (node == null) {
4544-
return true;
4545-
}
4546-
return switch (node.type) {
4547-
case ExpressionNode.CONSTANT -> true;
4548-
case ExpressionNode.OPERATION -> isCompileTimeConstant(node.lhs) && isCompileTimeConstant(node.rhs);
4549-
default -> false;
4550-
};
4551-
}
4552-
45534584
private JoinContext moveClauses(QueryModel parent, JoinContext from, JoinContext to, IntList positions) {
45544585
int p = 0;
45554586
int m = positions.size();

core/src/test/java/io/questdb/test/griffin/ExplainPlanTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4942,6 +4942,22 @@ public void testNoArgFalseConstantExpressionUsedInJoinIsOptimizedAway() throws E
49424942
});
49434943
}
49444944

4945+
@Test
4946+
public void testNoArgMixedConstAndRuntimeExprInJoin() throws Exception {
4947+
// When constWhereClause mixes compile-time false with a runtime expression,
4948+
// the optimizer keeps the compile-time false in constWhereClause and the
4949+
// code generator folds it to Empty table.
4950+
assertMemoryLeak(() -> {
4951+
execute("CREATE TABLE tab (b BOOLEAN, ts TIMESTAMP)");
4952+
assertPlanNoLeakCheck(
4953+
"SELECT * FROM tab T1 INNER JOIN tab T2 ON T1.b = T2.b WHERE 1 > 10 AND NOW() = NOW()",
4954+
"""
4955+
SelectedRecord
4956+
Empty table
4957+
""");
4958+
});
4959+
}
4960+
49454961
@Test
49464962
public void testNoArgNonConstantExpressionUsedInJoinClauseIsUsedAsPostJoinFilter() throws Exception {
49474963
node1.setProperty(PropertyKey.DEV_MODE_ENABLED, true);

core/src/test/java/io/questdb/test/griffin/engine/join/JoinTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,6 +2893,65 @@ public void testJoinInnerPostJoinAndConstFilter() throws Exception {
28932893
});
28942894
}
28952895

2896+
@Test
2897+
public void testJoinInnerPostJoinAndMixedConstFilter() throws Exception {
2898+
// When constWhereClause mixes compile-time and non-compile-time terms
2899+
// (e.g. false AND NOW() = NOW()), the optimizer splits them: false stays
2900+
// as constWhereClause and the code generator folds it to EmptyTableRecordCursorFactory.
2901+
assertMemoryLeak(() -> {
2902+
execute("CREATE TABLE t (val INT, ts TIMESTAMP) TIMESTAMP(ts) PARTITION BY DAY");
2903+
execute("INSERT INTO t VALUES (1, '2024-01-01T00:00:00.000000Z')");
2904+
assertQueryNoLeakCheck(
2905+
"val\tval1\n",
2906+
"SELECT T1.val, T2.val FROM t T1 " +
2907+
"INNER JOIN t T2 ON T1.ts < T2.ts " +
2908+
"WHERE T1.val > 0 AND 1 > 10 AND NOW() = NOW()",
2909+
null, false, true
2910+
);
2911+
});
2912+
}
2913+
2914+
@Test
2915+
public void testJoinInnerPostJoinAndMixedConstTrueFilter() throws Exception {
2916+
// When constWhereClause has true AND NOW() = NOW(), the optimizer merges
2917+
// NOW() = NOW() into postJoinWhereClause and the code generator folds
2918+
// the remaining constant true away.
2919+
assertMemoryLeak(() -> {
2920+
execute("CREATE TABLE t (val INT, ts TIMESTAMP) TIMESTAMP(ts) PARTITION BY DAY");
2921+
execute("""
2922+
INSERT INTO t VALUES
2923+
(1, '2024-01-01T00:00:00.000000Z'),
2924+
(2, '2024-01-02T00:00:00.000000Z')
2925+
""");
2926+
String query = "SELECT T1.val, T2.val FROM t T1 " +
2927+
"INNER JOIN t T2 ON T1.ts < T2.ts " +
2928+
"WHERE T1.val > 0 AND 1 < 10 AND NOW() = NOW()";
2929+
assertQueryNoLeakCheck(
2930+
"""
2931+
val\tval1
2932+
1\t2
2933+
""",
2934+
query,
2935+
null, false, false
2936+
);
2937+
// Verify: no Empty table (1 < 10 folded as constant true), and
2938+
// now()=now() merged from constWhereClause into a post-join filter.
2939+
assertPlanNoLeakCheck(query, """
2940+
SelectedRecord
2941+
Filter filter: (T1.ts<T2.ts and now()=now())
2942+
Cross Join
2943+
Async JIT Filter workers: 1
2944+
filter: 0<val
2945+
PageFrame
2946+
Row forward scan
2947+
Frame forward scan on: t
2948+
PageFrame
2949+
Row forward scan
2950+
Frame forward scan on: t
2951+
""");
2952+
});
2953+
}
2954+
28962955
@Test
28972956
public void testJoinInnerPostJoinMultipleJoinsFilter() throws Exception {
28982957
// Tests multi-way join with post-join WHERE conditions referencing

0 commit comments

Comments
 (0)