Skip to content

Commit 3283ee6

Browse files
committed
feat(clickhouse): Wave 2 coverage backlog query-structure oracles (#6,7,8,9,10)
Five self-contained query-structure oracles (default-on, --flag gated, raw-SQL, no general-fleet hot-path edits -- that emission is a separate gated pass): - #6 GroupingDecomposition GROUP BY WITH ROLLUP: detail(GROUPING=0)==plain GROUP BY, super-agg(GROUPING=1)==grand count(), sum(group counts)==grand - #7 LimitRanking LIMIT a,b==LIMIT b OFFSET a; LIMIT n prefix-of LIMIT n WITH TIES; LIMIT n BY k <= n rows/key (deterministic total ORDER BY) - #8 WindowFrame default frame==explicit RANGE/ROWS UNBOUNDED..CURRENT; lagInFrame ==one-preceding frame (unique-key fixture, single-snapshot compares) - #9 SemiJoinRewrite LEFT SEMI==IN, LEFT ANTI==NOT IN (preserved-side projection only per #107073), LEFT ANY JOIN cardinality==left count - #10 ColumnTransformer * EXCEPT/APPLY/COLUMNS(regex)==explicit list; DISTINCT ON cardinality Wired into factory/options/run-sqlancer.sh; mvn compile green. Probe-on-head (dev-vm): lagInFrame/leadInFrame, empty-frame NULL boundary, WITH TIES/LIMIT BY syntax.
1 parent f223905 commit 3283ee6

8 files changed

Lines changed: 1108 additions & 1 deletion

.claude/run-sqlancer.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ EXTRA_CH_ARGS=""
3535
# 26.x coverage oracles (TextIndexLike..StatsToggle) appended 2026-06-10 after their convergence
3636
# run: 3h x 41 oracles x 1.09M queries with --eet-26x-modes/--variant-where-emission on produced
3737
# 0 false positives and 1 genuine CH wrong-result (JoinReorder, ANTI/SEMI/INNER chain).
38-
ALL_ORACLES="TLPWhere,TLPDistinct,TLPGroupBy,TLPAggregate,TLPHaving,NoREC,PQS,CERT,CODDTest,SEMR,SEMRMulti,EET,SetOpTLP,CombinatorTLP,QccCache,SortedUnionLimitBy,SchemaRoundtrip,JoinAlgorithm,Cast,Parallelism,PartitionMirror,KeyCondition,TableFunctionIN,ViewEquivalence,AggregateStateRoundtrip,MaterializedViewConsistency,FinalMerge,ProjectionToggle,PatchPartConsistency,DictGetVsJoin,WindowEquivalence,DynamicSubcolumn,SubqueryMaterialize,MutationAnalyzer,TextIndexLike,TopK,JoinReorder,NaturalJoin,JsonSkipIndex,MaterializedCte,StatsToggle,ExtendedDatetime,JoinUseNulls,QueryCache,TextIndexDirectRead,TextIndexContainer,TextIndexLifecycle,PrewhereEquivalence,ReadInOrderToggle,CountOptimization,LazyMaterializationToggle,ReplacingDedup,QuantileConsistency,UniqExactness,ArgExtremum,MaterializedColumn"
38+
ALL_ORACLES="TLPWhere,TLPDistinct,TLPGroupBy,TLPAggregate,TLPHaving,NoREC,PQS,CERT,CODDTest,SEMR,SEMRMulti,EET,SetOpTLP,CombinatorTLP,QccCache,SortedUnionLimitBy,SchemaRoundtrip,JoinAlgorithm,Cast,Parallelism,PartitionMirror,KeyCondition,TableFunctionIN,ViewEquivalence,AggregateStateRoundtrip,MaterializedViewConsistency,FinalMerge,ProjectionToggle,PatchPartConsistency,DictGetVsJoin,WindowEquivalence,DynamicSubcolumn,SubqueryMaterialize,MutationAnalyzer,TextIndexLike,TopK,JoinReorder,NaturalJoin,JsonSkipIndex,MaterializedCte,StatsToggle,ExtendedDatetime,JoinUseNulls,QueryCache,TextIndexDirectRead,TextIndexContainer,TextIndexLifecycle,PrewhereEquivalence,ReadInOrderToggle,CountOptimization,LazyMaterializationToggle,ReplacingDedup,QuantileConsistency,UniqExactness,ArgExtremum,MaterializedColumn,GroupingDecomposition,LimitRanking,WindowFrame,SemiJoinRewrite,ColumnTransformer"
3939

4040
usage() {
4141
cat <<EOF

src/sqlancer/clickhouse/ClickHouseOptions.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ public class ClickHouseOptions implements DBMSSpecificOptions<ClickHouseOracleFa
9595
@Parameter(names = "--materialized-column-oracle", description = "MaterializedColumn oracle: each MATERIALIZED/ALIAS column's stored value == its defining expression recomputed in the same query (single-snapshot two-column compare).", arity = 1)
9696
public boolean materializedColumnOracle = true;
9797

98+
@Parameter(names = "--grouping-decomposition-oracle", description = "GroupingDecomposition oracle: GROUP BY WITH ROLLUP detail rows (GROUPING(k)=0) == plain GROUP BY, super-aggregate row (GROUPING(k)=1) == grand count(), and sum of per-group counts == grand count(); integer aggregates + non-float keys only.", arity = 1)
99+
public boolean groupingDecompositionOracle = true;
100+
101+
@Parameter(names = "--limit-ranking-oracle", description = "LimitRanking oracle: LIMIT a,b == LIMIT b OFFSET a, LIMIT n is a prefix of LIMIT n WITH TIES, and LIMIT n BY k yields <= n rows per distinct k. Deterministic total ORDER BY.", arity = 1)
102+
public boolean limitRankingOracle = true;
103+
104+
@Parameter(names = "--window-frame-oracle", description = "WindowFrame oracle: over a unique-key fixture, default frame == explicit RANGE/ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and lagInFrame offsets match a one-preceding frame (single-snapshot column compares).", arity = 1)
105+
public boolean windowFrameOracle = true;
106+
107+
@Parameter(names = "--semi-join-rewrite-oracle", description = "SemiJoinRewrite oracle: LEFT SEMI JOIN (preserved-side projection only, per #107073) == WHERE k IN (subquery), LEFT ANTI JOIN == NOT IN, and LEFT ANY JOIN cardinality == left row count.", arity = 1)
108+
public boolean semiJoinRewriteOracle = true;
109+
110+
@Parameter(names = "--column-transformer-oracle", description = "ColumnTransformer oracle: SELECT * EXCEPT/APPLY/COLUMNS(regex) == the explicit column list, and DISTINCT ON (k) cardinality == count(DISTINCT k).", arity = 1)
111+
public boolean columnTransformerOracle = true;
112+
98113
@Override
99114
public List<ClickHouseOracleFactory> getTestOracleFactory() {
100115
return oracle;

src/sqlancer/clickhouse/ClickHouseOracleFactory.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
import sqlancer.clickhouse.oracle.aggfamily.ClickHouseUniqExactnessOracle;
6161
import sqlancer.clickhouse.oracle.aggfamily.ClickHouseArgExtremumOracle;
6262
import sqlancer.clickhouse.oracle.matcol.ClickHouseMaterializedColumnOracle;
63+
import sqlancer.clickhouse.oracle.groupby.ClickHouseGroupingDecompositionOracle;
64+
import sqlancer.clickhouse.oracle.limit.ClickHouseLimitRankingOracle;
65+
import sqlancer.clickhouse.oracle.window.ClickHouseWindowFrameOracle;
66+
import sqlancer.clickhouse.oracle.join.ClickHouseSemiJoinRewriteOracle;
67+
import sqlancer.clickhouse.oracle.transform.ClickHouseColumnTransformerOracle;
6368
import sqlancer.common.oracle.NoRECOracle;
6469
import sqlancer.common.oracle.TLPWhereOracle;
6570
import sqlancer.common.oracle.TestOracle;
@@ -453,5 +458,40 @@ public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalStat
453458
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
454459
return new ClickHouseMaterializedColumnOracle(globalState);
455460
}
461+
},
462+
GroupingDecomposition {
463+
464+
@Override
465+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
466+
return new ClickHouseGroupingDecompositionOracle(globalState);
467+
}
468+
},
469+
LimitRanking {
470+
471+
@Override
472+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
473+
return new ClickHouseLimitRankingOracle(globalState);
474+
}
475+
},
476+
WindowFrame {
477+
478+
@Override
479+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
480+
return new ClickHouseWindowFrameOracle(globalState);
481+
}
482+
},
483+
SemiJoinRewrite {
484+
485+
@Override
486+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
487+
return new ClickHouseSemiJoinRewriteOracle(globalState);
488+
}
489+
},
490+
ColumnTransformer {
491+
492+
@Override
493+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
494+
return new ClickHouseColumnTransformerOracle(globalState);
495+
}
456496
}
457497
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package sqlancer.clickhouse.oracle.groupby;
2+
3+
import java.sql.SQLException;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.TreeMap;
8+
import java.util.stream.Collectors;
9+
10+
import com.clickhouse.data.ClickHouseDataType;
11+
12+
import sqlancer.ComparatorHelper;
13+
import sqlancer.IgnoreMeException;
14+
import sqlancer.Randomly;
15+
import sqlancer.clickhouse.ClickHouseErrors;
16+
import sqlancer.clickhouse.ClickHouseProvider.ClickHouseGlobalState;
17+
import sqlancer.clickhouse.ClickHouseSchema.ClickHouseColumn;
18+
import sqlancer.clickhouse.ClickHouseSchema.ClickHouseTable;
19+
import sqlancer.common.oracle.TestOracle;
20+
import sqlancer.common.query.ExpectedErrors;
21+
22+
public class ClickHouseGroupingDecompositionOracle implements TestOracle<ClickHouseGlobalState> {
23+
24+
private final ClickHouseGlobalState state;
25+
private final ExpectedErrors readErrors = new ExpectedErrors();
26+
27+
public ClickHouseGroupingDecompositionOracle(ClickHouseGlobalState state) {
28+
this.state = state;
29+
ClickHouseErrors.addExpectedExpressionErrors(readErrors);
30+
ClickHouseErrors.addSessionSettingsErrors(readErrors);
31+
readErrors.add("UNKNOWN_TABLE");
32+
readErrors.add("Unknown table expression identifier");
33+
readErrors.add("UNKNOWN_IDENTIFIER");
34+
readErrors.add("Missing columns");
35+
readErrors.add("(MEMORY_LIMIT_EXCEEDED)");
36+
readErrors.add("memory limit exceeded");
37+
readErrors.add("TIMEOUT_EXCEEDED");
38+
readErrors.add("Timeout exceeded");
39+
readErrors.add("Limit for result exceeded");
40+
readErrors.add("TOO_MANY_ROWS_OR_BYTES");
41+
readErrors.add("ILLEGAL_AGGREGATION");
42+
readErrors.add("NOT_AN_AGGREGATE");
43+
readErrors.add("Aggregate function GROUPING");
44+
readErrors.add("Unknown function GROUPING");
45+
}
46+
47+
@Override
48+
public void check() throws SQLException {
49+
if (!state.getClickHouseOptions().groupingDecompositionOracle) {
50+
throw new IgnoreMeException();
51+
}
52+
List<ClickHouseTable> tables = state.getSchema().getRandomTableNonEmptyTables().getTables();
53+
if (tables.isEmpty()) {
54+
throw new IgnoreMeException();
55+
}
56+
ClickHouseTable table = Randomly.fromList(tables);
57+
if (table.isView()) {
58+
throw new IgnoreMeException();
59+
}
60+
List<ClickHouseColumn> eligible = table.getColumns().stream()
61+
.filter(c -> !c.isAlias() && !c.isMaterialized())
62+
.filter(c -> isNonFloatScalarKey(c.getType().getType())).collect(Collectors.toList());
63+
if (eligible.isEmpty()) {
64+
throw new IgnoreMeException();
65+
}
66+
67+
String tableQ = quote(table.getName());
68+
ClickHouseColumn keyColumn = Randomly.fromList(eligible);
69+
String keyRef = quote(keyColumn.getName());
70+
String label = "key=" + keyColumn.getName() + " table=" + table.getName();
71+
72+
checkRollupDetailEqualsPlain(tableQ, keyRef, label);
73+
checkRollupSuperAggregateEqualsGrandTotal(tableQ, keyRef, label);
74+
checkPerGroupCountSumEqualsGrandTotal(tableQ, keyRef, label);
75+
}
76+
77+
private void checkRollupDetailEqualsPlain(String tableQ, String keyRef, String label) throws SQLException {
78+
String plain = "SELECT toString(tuple(" + keyRef + ", count())) FROM " + tableQ + " GROUP BY " + keyRef;
79+
String rollupDetail = "SELECT toString(tuple(" + keyRef + ", count())) FROM " + tableQ + " GROUP BY " + keyRef
80+
+ " WITH ROLLUP HAVING GROUPING(" + keyRef + ") = 0";
81+
82+
List<String> plainRows = ComparatorHelper.getResultSetFirstColumnAsString(plain, readErrors, state);
83+
List<String> rollupRows = ComparatorHelper.getResultSetFirstColumnAsString(rollupDetail, readErrors, state);
84+
List<String> diff = multisetDiff(plainRows, rollupRows);
85+
if (!diff.isEmpty()) {
86+
throw new AssertionError(String.format(
87+
"grouping-decomposition ROLLUP-detail vs plain multiset mismatch [%s]:%n plain (%d rows): %s%n "
88+
+ "rollup-detail (%d rows): %s%n first differing entries: %s",
89+
label, plainRows.size(), plain, rollupRows.size(), rollupDetail, diff));
90+
}
91+
}
92+
93+
static List<String> multisetDiff(List<String> left, List<String> right) {
94+
Map<String, Long> counts = new TreeMap<>();
95+
for (String s : left) {
96+
counts.merge(s == null ? "\\N" : s, 1L, Long::sum);
97+
}
98+
for (String s : right) {
99+
counts.merge(s == null ? "\\N" : s, -1L, Long::sum);
100+
}
101+
List<String> diff = new ArrayList<>();
102+
for (Map.Entry<String, Long> e : counts.entrySet()) {
103+
if (e.getValue() == 0) {
104+
continue;
105+
}
106+
if (diff.size() >= 20) {
107+
break;
108+
}
109+
long c = e.getValue();
110+
diff.add(e.getKey() + " (+" + Math.abs(c) + " " + (c > 0 ? "plain" : "rollup") + ")");
111+
}
112+
return diff;
113+
}
114+
115+
private void checkRollupSuperAggregateEqualsGrandTotal(String tableQ, String keyRef, String label)
116+
throws SQLException {
117+
String rollupSuper = "SELECT toString(count()) FROM " + tableQ + " GROUP BY " + keyRef
118+
+ " WITH ROLLUP HAVING GROUPING(" + keyRef + ") = 1";
119+
String grand = "SELECT toString(count()) FROM " + tableQ;
120+
121+
String superValue = readSingleValue(rollupSuper);
122+
String grandValue = readSingleValue(grand);
123+
if (!grandValue.equals(superValue)) {
124+
throw new AssertionError(String.format(
125+
"grouping-decomposition ROLLUP super-aggregate mismatch [%s]:%n super: %s -> %s%n grand: %s -> %s",
126+
label, rollupSuper, superValue, grand, grandValue));
127+
}
128+
}
129+
130+
private void checkPerGroupCountSumEqualsGrandTotal(String tableQ, String keyRef, String label)
131+
throws SQLException {
132+
String perGroupSum = "SELECT toString(sum(g)) FROM (SELECT count() AS g FROM " + tableQ + " GROUP BY " + keyRef
133+
+ ")";
134+
String grand = "SELECT toString(count()) FROM " + tableQ;
135+
136+
String sumValue = readSingleValue(perGroupSum);
137+
String grandValue = readSingleValue(grand);
138+
if (!grandValue.equals(sumValue)) {
139+
throw new AssertionError(String.format(
140+
"grouping-decomposition per-group-count-sum mismatch [%s]:%n sum: %s -> %s%n grand: %s -> %s",
141+
label, perGroupSum, sumValue, grand, grandValue));
142+
}
143+
}
144+
145+
private String readSingleValue(String query) throws SQLException {
146+
List<String> rows = ComparatorHelper.getResultSetFirstColumnAsString(query, readErrors, state);
147+
if (rows.size() != 1) {
148+
throw new IgnoreMeException();
149+
}
150+
return rows.get(0);
151+
}
152+
153+
static boolean isNonFloatScalarKey(ClickHouseDataType t) {
154+
switch (t) {
155+
case Int8:
156+
case Int16:
157+
case Int32:
158+
case Int64:
159+
case Int128:
160+
case Int256:
161+
case UInt8:
162+
case UInt16:
163+
case UInt32:
164+
case UInt64:
165+
case UInt128:
166+
case UInt256:
167+
case String:
168+
case FixedString:
169+
case Date:
170+
case Date32:
171+
case DateTime:
172+
case DateTime32:
173+
case DateTime64:
174+
case UUID:
175+
return true;
176+
default:
177+
return false;
178+
}
179+
}
180+
181+
static String quote(String identifier) {
182+
return "`" + identifier.replace("`", "``") + "`";
183+
}
184+
}

0 commit comments

Comments
 (0)