Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions .claude/CLAUDE.md

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion .claude/run-sqlancer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@ EXTRA_CH_ARGS=""
# factory but had drifted out of this list (never ran under --oracles all); re-added 2026-06-10.
# ExtendedDatetime/JoinUseNulls/QueryCache appended 2026-06-11 (settings-coverage plan section 3/5
# targeted oracles; tmp/ch-settings-to-test-in-sqlancer.md).
# FloatPruning is DELIBERATELY absent from ALL_ORACLES (2026-08-15): it is a positive-control
# detector for ClickHouse #113417/#112036 (NaN rows dropped by float part pruning under a negated
# comparison), which still reproduce on head at DEFAULT settings, so it asserts on essentially every
# iteration -- a 6-minute standalone run produced 326 worker deaths over 175 queries. A constantly
# firing oracle orphans a database per iteration and wedges CH under a squeezed -m cap (that is what
# stalled the 2026-06-14 20h run via TextIndexDirectRead). Run it standalone with
# `--oracles FloatPruning`, and add it back here once those two issues close.
# 26.x coverage oracles (TextIndexLike..StatsToggle) appended 2026-06-10 after their convergence
# run: 3h x 41 oracles x 1.09M queries with --eet-26x-modes/--variant-where-emission on produced
# 0 false positives and 1 genuine CH wrong-result (JoinReorder, ANTI/SEMI/INNER chain).
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,EngineEquivalence,CoalescingFinal,JoinGetSet,RemoteLocalEquivalence,MapTupleContainer,GeoMetamorphic,VariantSubcolumn,AggregateStateExpansion,SequenceFunnel,PartitionLifecycle,AlterModifyConsistency,TtlDeterminism,InsertDedup,TokenBf,VectorIndexRecall,SampleClause,DistributedTable,AsofJoin,CubeGroupingSets,PasteJoin,CorrelatedSubquery,BitFunction,ArrayFunction,StringFunction,AggregateFunctionColumn,TimezoneDatetime,ArrayJoinUnfold,WindowFrameGroundTruth,JoinUsing,WithFill,SettingFlip,ConcurrentMutation,LowCardinalityEquivalence"
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,EngineEquivalence,CoalescingFinal,JoinGetSet,RemoteLocalEquivalence,MapTupleContainer,GeoMetamorphic,VariantSubcolumn,AggregateStateExpansion,SequenceFunnel,PartitionLifecycle,AlterModifyConsistency,TtlDeterminism,InsertDedup,TokenBf,VectorIndexRecall,SampleClause,DistributedTable,AsofJoin,CubeGroupingSets,PasteJoin,CorrelatedSubquery,BitFunction,ArrayFunction,StringFunction,AggregateFunctionColumn,TimezoneDatetime,ArrayJoinUnfold,WindowFrameGroundTruth,JoinUsing,WithFill,SettingFlip,ConcurrentMutation,LowCardinalityEquivalence,DistributedPlanEquivalence,CodecRoundtrip,PipeEquivalence,IEJoin,TupleFinalAggregation"

usage() {
cat <<EOF
Expand Down
127 changes: 106 additions & 21 deletions docs/plans/2026-08-15-001-feat-clickhouse-4month-coverage-gap-plan.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/sqlancer/clickhouse/ClickHouseErrors.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,16 @@ public static List<String> getExpectedExpressionErrors() {

"CAST AS FixedString is only implemented", "default expression and column type are incompatible",

"Incompatible data types between aggregate function", "NOT_IMPLEMENTED");
"Incompatible data types between aggregate function", "NOT_IMPLEMENTED",

"does not support PREWHERE", "ILLEGAL_PREWHERE", "is not supported by storage View");
}

public static void addExpectedExpressionErrors(ExpectedErrors errors) {
errors.addAll(getExpectedExpressionErrors());

errors.addAll(getKnownOpenJoinOrderBugs());

errors.addAll(getStatisticsErrors());

errors.addAll(getEnumErrors());
Expand Down Expand Up @@ -247,6 +251,10 @@ public static List<String> getKnownOpenMutationAnalyzerBugs() {
return List.of("is already registered");
}

public static List<String> getKnownOpenJoinOrderBugs() {
return List.of("Left and right columns have same names");
}

public static boolean isToleratedException(Throwable e) {
ExpectedErrors errors = ExpectedErrors.newErrors().with(getExpectedExpressionErrors())
.with(getSessionSettingsErrors()).build();
Expand Down
51 changes: 51 additions & 0 deletions src/sqlancer/clickhouse/ClickHouseOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ public class ClickHouseOptions implements DBMSSpecificOptions<ClickHouseOracleFa
@Parameter(names = "--text-search-predicate-emission", description = "Emit full-text-search predicates (hasToken/hasAllTokens/hasAnyTokens/startsWith/endsWith/multiSearchAny) over plain String columns in general WHERE context, from a fixed token vocabulary. Lets the whole oracle fleet (TLPWhere/NoREC/CODDTest/...) incidentally differential-test text-indexed columns against full scans. Restricted to startsWith/endsWith/multiSearchAny (the functions proven index==scan-equivalent across all tokenizers); hasToken/hasAllTokens/hasAnyTokens are NOT emitted here because they diverge index-vs-scan on array/ngrams/preprocessor (ClickHouse#107186), which the dedicated TextIndexDirectRead oracle targets instead.", arity = 1)
public boolean textSearchPredicateEmission = true;

@Parameter(names = "--persistent-view-emission", description = "Create plain VIEWs (named v<n>, so the schema reader marks them as views) as a DDL action during database generation, capped at 3 per database, so a VIEW can be picked as one relation of a multi-relation FROM list. Four open ClickHouse bugs need exactly that shape, including #114113 (LOGICAL_ERROR 'Left and right columns have same names' out of chooseJoinOrder for a three-way comma join whose middle relation is a view). Write-path generators and oracles filter views out, so this only widens the read surface.", arity = 1)
public boolean persistentViewEmission = true;

@Parameter(names = "--comma-join-emission", description = "Let the join generator emit genuine ON-less CROSS (comma) joins and chains of up to four relations, instead of always attaching an ON clause to a CROSS join (which silently degraded every CROSS into an INNER join). Required for the #114113 shape. Rate-limited to 10% of CROSS picks on purpose: at 50% a 40-minute dev-VM run spent about 40% of its thread budget on three- and four-way cartesian products that time out at max_execution_time, and throughput fell from ~100 to ~10 queries/s.", arity = 1)
public boolean commaJoinEmission = true;

@Parameter(names = "--truth-value-predicate-emission", description = "Emit boolean-position wrappers and SQL truth-value predicates in general WHERE context: NOT (NOT x), NOT x, x IS [NOT] TRUE/FALSE/UNKNOWN, x IS NOT DISTINCT FROM lit, nullIf/ifNull/coalesce(x, lit), and LIKE/ILIKE ... ESCAPE. Half the time the wrapper is compared against a numeric or float constant, which puts a boolean-valued expression in value position -- the shape KeyCondition's inversion pushdown mishandles (NOT (NOT key) collapses to bare key and prunes valid parts). Feeds the already-sound KeyCondition/NoREC/TLPWhere oracles; generator-only, no new comparison logic.", arity = 1)
public boolean truthValuePredicateEmission = true;

@Parameter(names = "--join-reorder-allow-dropped-key-ref", description = "Let the JoinReorder oracle build ON clauses that reference a key column dropped by a preceding SEMI/ANTI join. Default false, PERMANENTLY: ClickHouse#107073 was closed by the optimizer team as by-design non-determinism -- columns read from the eliminated side of a SEMI/ANTI join are ANY-like (filled from whichever matching row arrives first), so any plan change or physical row-order change legally flips the result and a differential oracle comparing such queries is unsound. The restriction is therefore a soundness rule, not a temporary known-bug pin. Set true only to demonstrate the documented non-determinism.", arity = 1)
public boolean joinReorderAllowDroppedKeyRef = false;

Expand Down Expand Up @@ -162,6 +171,15 @@ public class ClickHouseOptions implements DBMSSpecificOptions<ClickHouseOracleFa
public boolean distributedTableOracle = true;


@Parameter(names = "--codec-roundtrip-oracle", description = "CodecRoundtrip oracle: a table whose columns carry random per-type CODEC(...) declarations and a CODEC(NONE) mirror holding the same inserted rows (including NaN, +/-inf, -0.0 and denormals) must answer the same read identically, still after OPTIMIZE ... FINAL, and still after an ALTER TABLE ... MODIFY COLUMN ... CODEC mutation. Lossy codecs are excluded from the equality arm by an explicit allowlist and only have their row count and NULL mask asserted.", arity = 1)
public boolean codecRoundtripOracle = true;

@Parameter(names = "--float-pruning-oracle", description = "FloatPruning oracle: over a private fixture whose Float32/Float64/Nullable(Float64) columns hold NaN, +/-inf, -0.0 and NULL across several parts (one part all-NaN), with float ORDER BY / PARTITION BY / minmax + bloom_filter skip indexes / materialized statistics, a negated float comparison must select the same key multiset with pruning enabled as with materialize() plus use_skip_indexes / allow_statistics_optimize / convert_query_to_cnf / optimize_move_to_prewhere all off, and count(P) + count(NOT P) + count(P IS NULL) must equal count(*). Row sets only, never a float aggregate, so the exact-integer-aggregate rule is not violated.", arity = 1)
public boolean floatPruningOracle = true;

@Parameter(names = "--distributed-plan-equivalence-oracle", description = "DistributedPlanEquivalence oracle: one generated read must return the same multiset under plain local execution, make_distributed_plan = 1, serialize_query_plan = 1, a cluster('default', ...) read with parallel_replicas_local_plan on and off, and enable_parallel_replicas + max_parallel_replicas > 1 over both the local and the Distributed relation. Self-contained multi-block fixture that includes a VIEW relation in a three-way comma join, the shape behind #111727.", arity = 1)
public boolean distributedPlanEquivalenceOracle = true;

@Parameter(names = "--with-fill-oracle", description = "WithFill oracle: ORDER BY x WITH FILL FROM f TO t STEP s over a private Int64 table whose inserted rows are a subset of the step grid must return exactly the full grid [f, t) in ascending order -- present rows are kept, absent grid points are synthesized, no duplicates, no off-grid rows.", arity = 1)
public boolean withFillOracle = true;

Expand Down Expand Up @@ -210,6 +228,39 @@ public class ClickHouseOptions implements DBMSSpecificOptions<ClickHouseOracleFa
@Parameter(names = "--low-cardinality-equivalence-oracle", description = "LowCardinalityEquivalence oracle: over a private fixture with paired plain/LowCardinality columns (Int32, String, Nullable(Int32), FixedString(4)) holding identical values, any read (row projection / GROUP BY / uniqExact / predicate) over the plain columns must equal the same read over the LowCardinality twins.", arity = 1)
public boolean lowCardinalityEquivalenceOracle = true;

@Parameter(names = "--groups-window-frame-emission", description = "Let the window-function generator attach an explicit frame clause, including the GROUPS frame mode added by PR #108653 (offsets count peer groups, not rows). ROWS and RANGE frames are emitted too; before this the generator emitted no frame clause at all, so every window call used the implicit default frame. The WindowFrameGroundTruth oracle grows a matching GROUPS arm with a tie-forming fixture and a Java peer-group ground truth.", arity = 1)
public boolean groupsWindowFrameEmission = true;

@Parameter(names = "--negative-limit-emission", description = "Let the LimitRanking oracle emit the negative LIMIT forms (LIMIT -n, LIMIT -n BY k, LIMIT -n WITH TIES) added by PR #103222 / PR #100930 and rewritten by PR #106502. LIMIT -n BY k takes the LAST n rows per key, so the sound assertion is that it equals LIMIT n BY k over the reverse total order (one fixture, multiset compare).", arity = 1)
public boolean negativeLimitEmission = true;

@Parameter(names = "--comparison-chain-emission", description = "Emit long homogeneous predicate chains -- 'col LIKE a% OR col LIKE b% OR ...' and 'col != 1 AND col != 2 AND ... AND col < n' (sometimes with a deliberately conflicting conjunct) -- so that optimize_or_like_chain (default-on since PR #94517) and optimize_and_compare_chain (PR #99736) actually fire. Without a chain shape neither rewrite is reachable. The SettingFlip oracle gains a dedicated arm that toggles both plus convert_query_to_cnf over such a chain.", arity = 1)
public boolean comparisonChainEmission = true;

@Parameter(names = "--index-hint-emission", description = "Enable the KeyCondition oracle's indexHint arm: rows(P AND Q) must be a sub-multiset of rows(indexHint(P) AND Q), which in turn must be a sub-multiset of rows(Q). indexHint(P) does NOT evaluate P as a filter, but it is not result-neutral either -- it restricts the read to the granules index analysis selects for P, so a row outside those granules is legitimately dropped (measured on head 26.8.1.1471). The lower bound is the pruning-soundness assertion that matters: a row satisfying P AND Q that indexHint(P) AND Q loses means index analysis pruned a granule holding a matching row. Deliberately NOT emitted into the general fleet's generatePredicate: with granule-level semantics inside a TLP partition, P / NOT P / P IS NULL read different granule sets and their union is no longer the whole table.", arity = 1)
public boolean indexHintEmission = true;

@Parameter(names = "--sparse-column-emission", description = "Make sparse serialization actually engage in fuzzed tables: set ratio_of_defaults_for_sparse_serialization explicitly at a low value in CREATE TABLE about half the time, and bias a random subset of columns of plain-MergeTree tables overwhelmingly towards the type default on INSERT. Sparse columns have a separate read path, a separate default-filling path and (since PR #105890) separate pruning and trivial-count logic; every existing pruning/count/FINAL oracle then covers them for free. Never applied to a dedupe engine's table, so the C2 rule on key-domain degeneracy is untouched.", arity = 1)
public boolean sparseColumnEmission = true;

@Parameter(names = "--mixed-direction-sorting-key", description = "Let the table generator emit descending and mixed-direction sorting keys (ORDER BY (a, b DESC), ORDER BY a DESC). Read-in-order and aggregation-in-order only take their non-uniform code path for such a key, which is where #111901 (optimize_aggregation_in_order over (a, b DESC) collapses GROUP BY groups) lives. A DESC key suppresses the PRIMARY KEY prefix and SAMPLE BY clauses for that table, and is never emitted for a dedupe engine.", arity = 1)
public boolean mixedDirectionSortingKey = true;

@Parameter(names = "--text-index-second-wave", description = "Enable the second-wave text-index arms: the icu('<locale>') tokenizer, hasPhrase with a Java token-position ground truth (needs allow_experimental_text_index_phrase_search = 1), the trivial-count-from-text-index arm (query_plan_optimize_count_from_text_index on vs off vs use_skip_indexes = 0), and text index parameters supplied via table settings instead of inline index arguments. The Japanese/MeCab tokenizer is deliberately absent: it needs a server-side <tokenizer><japanese> dictionary that the fuzzer's container does not carry.", arity = 1)
public boolean textIndexSecondWave = true;

@Parameter(names = "--pipe-equivalence-oracle", description = "PipeEquivalence oracle: one generated single-relation read rendered in classic SQL and in pipe-operator syntax (PR #111151) must return the same rows. Second renderer over one AST, the MaterializedColumnVisitor pattern. Restricted to a single relation on purpose -- each pipe stage is wrapped in a subquery, so a qualified name from a multi-relation FROM list stops resolving after the first stage.", arity = 1)
public boolean pipeEquivalenceOracle = true;

@Parameter(names = "--ie-join-oracle", description = "IEJoin oracle: a join whose ON has two inequality comparisons (the shape that activates the IEJoin algorithm of PR #109920) must return the same rows as the equivalent CROSS JOIN + WHERE. Other join algorithms cannot answer that ON shape at all (INVALID_JOIN_ON_EXPRESSION), so the cross-join rewrite, not an algorithm sweep, is the reference arm. Small fixtures by construction, since a non-equi join is quadratic.", arity = 1)
public boolean ieJoinOracle = true;

@Parameter(names = "--tuple-final-aggregation-oracle", description = "TupleFinalAggregation oracle: per-element aggregation of Tuple columns in SummingMergeTree and CoalescingMergeTree (PR #98039, gated by the allow_tuple_element_aggregation table setting) == a Java element-wise ground truth, and query-time FINAL == the result after a physical OPTIMIZE ... FINAL. Integer tuple elements only (C3), full projections only. AggregatingMergeTree is excluded: a plain Tuple is not an aggregate state there, so its FINAL legitimately keeps the first row.", arity = 1)
public boolean tupleFinalAggregationOracle = true;

@Parameter(names = "--summing-subset-projection-arm", description = "Enable the TupleFinalAggregation oracle's subset-projection arm, which reads only some of a SummingMergeTree's summed columns under query-time FINAL. Default false: this is a positive-control detector for the still-open #106125 (query-time FINAL applies the all-zero-row-deletion rule over only the columns the query reads), which reproduces on every current head, so with the arm on the oracle asserts on essentially every iteration -- the FloatPruning/TextIndexDirectRead situation. Turn it on to demonstrate #106125, not for fleet runs.", arity = 1)
public boolean summingSubsetProjectionArm;

@Override
public List<ClickHouseOracleFactory> getTestOracleFactory() {
return oracle;
Expand Down
Loading
Loading