Skip to content

Commit 9a411cf

Browse files
committed
feat(clickhouse): deepen text-index / full-text-search testing (6 units)
Extends the existing TextIndexLike + JsonSkipIndex coverage toward the full ClickHouse text-index surface. Validated on dev-vm head 26.6.1.734: a 1-hour full-fleet run (167k queries, all ~45 oracles) produced 0 false positives from any of these units. Soundness rules below were established empirically there. ClickHouse#3 Pure-optimization toggle arm: TextIndexLike gains a DIRECT_READ_OFF arm (query_plan_direct_read_from_text_index=0, query_plan_text_index_add_hint=0). A pure optimization toggle must never change results; UNKNOWN_SETTING is already tolerated so it degrades on older builds. ClickHouse#4 Merge + delete-masked-part topology: TextIndexLike optionally issues a synchronous lightweight DELETE (lightweight_deletes_sync=2) and/or OPTIMIZE TABLE ... FINAL after the inserts, recording topology in assertion messages. The Java ground truth now counts over the live (non-deleted) rows. Targets the #107309 class (index honoring a delete-masked part). ClickHouse#6 General-fleet predicate injection: generateTextSearchPredicate emits startsWith / endsWith / multiSearchAny over plain String columns from a fixed vocabulary, gated by --text-search-predicate-emission (default on). These three are the functions proven index==scan-equivalent across ALL tokenizers (incl. array). hasToken/hasAllTokens/hasAnyTokens are deliberately NOT emitted here: hasToken diverges index-vs-scan on the array tokenizer (whole-value token) and hasAllTokens/hasAnyTokens diverge on ngrams (the multi-token needle's space-spanning grams) -- by-design tokenizer-semantics differences, not bugs. Sound for NoREC because renderSkipIndex never carries a preprocessor. #7a renderSkipIndex breadth: full tokenizer matrix (splitByNonAlpha, ngrams(N), array, asciiCJK, splitByString, sparseGrams) and text() eligibility extended to FixedString / LowCardinality(String) / Nullable(String) / Array(String) (bare) and Map(String,*) via mapKeys/mapValues expression indexes. New ClickHouseErrors.getTextIndexErrors() tolerances wired into table creation. ClickHouse#5 TextIndexPreprocessor oracle (NEW): builds a private table with INDEX(s) preprocessor=lower(s), then asserts that a forced direct read (force_data_skipping_indices + direct_read=1, add_hint=0) over a mixed-case corpus equals a Java lower()-token-membership ground truth. (The documented INDEX(lower(s)) equivalent form cannot be force-engaged for hasToken(s,...) on 26.6.1.734 -- it raises INDEX_NOT_USED -- so the ground-truth comparison replaces a second-table comparison.) #7b TextIndexContainer oracle (NEW): Array(String) + array tokenizer with exact has/hasAny/hasAll List ground truth, and Map(String,String) key-vs-value isolation via mapContainsKey/mapContainsValue, across DEFAULT / ignore_data_skipping_indices / use_skip_indexes=0 arms. ALTER lifecycle: ClickHouseAlterGenerator gains ADD_INDEX / MATERIALIZE_INDEX / CLEAR_INDEX / DROP_INDEX kinds. New ClickHouseTextIndexLifecycle oracle asserts CREATE-with-index == (index-free + ALTER ADD INDEX + MATERIALIZE INDEX SETTINGS mutations_sync=2) == use_skip_indexes=0 scan over a predicate battery (LIKE + hasToken always; hasAllTokens/hasAnyTokens only on the splitByNonAlpha arm, where they are index==scan-equivalent), exercising the distinct MATERIALIZE-over-historical-parts code path. All three new oracles registered in ClickHouseOracleFactory and the run / validity oracle-name lists. All oracles use private per-iteration tables, fixed token vocabulary, and integer-key/count comparisons to stay clear of the repo's known false-positive families.
1 parent 671b1ea commit 9a411cf

13 files changed

Lines changed: 878 additions & 25 deletions

.claude/collect-oracle-validity.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ THREADS="${THREADS:-6}"
2020
OUTDIR="$ROOT/val"
2121

2222
# Full current oracle set (from run-sqlancer.sh ALL_ORACLES).
23-
IFS=',' read -r -a 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"
23+
IFS=',' read -r -a 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,TextIndexPreprocessor,TextIndexContainer,TextIndexLifecycle"
2424

2525
rm -rf "$OUTDIR"; mkdir -p "$OUTDIR"
2626

.claude/oracle-validity-workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const ORACLES = [
1616
'FinalMerge', 'ProjectionToggle', 'PatchPartConsistency', 'DictGetVsJoin', 'WindowEquivalence',
1717
'DynamicSubcolumn', 'SubqueryMaterialize', 'MutationAnalyzer', 'TextIndexLike', 'TopK', 'JoinReorder',
1818
'NaturalJoin', 'JsonSkipIndex', 'MaterializedCte', 'StatsToggle', 'ExtendedDatetime', 'JoinUseNulls',
19-
'QueryCache',
19+
'QueryCache', 'TextIndexPreprocessor', 'TextIndexContainer', 'TextIndexLifecycle',
2020
]
2121

2222
const VERDICT_SCHEMA = {

.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"
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,TextIndexPreprocessor,TextIndexContainer,TextIndexLifecycle"
3939

4040
usage() {
4141
cat <<EOF

src/sqlancer/clickhouse/ClickHouseErrors.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,26 @@ public static List<String> getAlterErrors() {
190190
"EMPTY_LIST_OF_COLUMNS_QUERIED",
191191

192192
"Projection with name", "NO_SUCH_PROJECTION_IN_TABLE", "ILLEGAL_PROJECTION",
193-
"Projection is fully supported", "projection", "Cannot add projection");
193+
"Projection is fully supported", "projection", "Cannot add projection",
194+
195+
"Cannot add index", "index with name", "NO_SUCH_DATA_PART", "already exists");
194196
}
195197

196198
public static void addAlterErrors(ExpectedErrors errors) {
197199
errors.addAll(getAlterErrors());
198200
}
199201

202+
public static List<String> getTextIndexErrors() {
203+
return List.of("Unknown Index type", "Unknown index type", "Unknown tokenizer",
204+
"Unexpected text index arguments", "full-text index", "full_text_index", "SUPPORT_IS_DISABLED",
205+
"INCORRECT_QUERY", "ILLEGAL_TYPE_OF_ARGUMENT", "ILLEGAL_INDEX", "BAD_ARGUMENTS",
206+
"data type must be String", "is not supported by text index", "of text index");
207+
}
208+
209+
public static void addTextIndexErrors(ExpectedErrors errors) {
210+
errors.addAll(getTextIndexErrors());
211+
}
212+
200213
public static List<String> getMutationErrors() {
201214
return List.of("TIMEOUT_EXCEEDED", "Cannot UPDATE key column", "Cannot DELETE", "Mutation cannot be executed",
202215

src/sqlancer/clickhouse/ClickHouseOptions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public class ClickHouseOptions implements DBMSSpecificOptions<ClickHouseOracleFa
5959
@Parameter(names = "--variant-where-emission", description = "Emit Variant-typed predicate fragments in WHERE context (26.1 Variant-in-all-functions surface, PR #90900 + use_variant_as_common_type default-on, PR #90677). WHERE-only by design: the client-v2 RowBinary reader cannot decode a projected Variant column (R4), so the fragments are self-contained Boolean expressions and never reach a fetch column. Default-on since the 2026-06-10 convergence run (0 reader deaths, 0 false positives; the toInt64 constant-fallback wrap is load-bearing).", arity = 1)
6060
public boolean variantWhereEmission = true;
6161

62+
@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. Sound for NoREC because the general schema never carries a preprocessor on a text index (index path == scan path for preprocessor-free hasToken); preprocessor coverage lives only in the dedicated TextIndexPreprocessor oracle.", arity = 1)
63+
public boolean textSearchPredicateEmission = true;
64+
6265
@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)
6366
public boolean joinReorderAllowDroppedKeyRef = false;
6467

src/sqlancer/clickhouse/ClickHouseOracleFactory.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
import sqlancer.clickhouse.oracle.mutate.ClickHouseMutationAnalyzerOracle;
2525
import sqlancer.clickhouse.oracle.patch.ClickHousePatchPartConsistencyOracle;
2626
import sqlancer.clickhouse.oracle.stats.ClickHouseStatsToggleOracle;
27+
import sqlancer.clickhouse.oracle.textindex.ClickHouseTextIndexContainerOracle;
28+
import sqlancer.clickhouse.oracle.textindex.ClickHouseTextIndexLifecycleOracle;
2729
import sqlancer.clickhouse.oracle.textindex.ClickHouseTextIndexLikeOracle;
30+
import sqlancer.clickhouse.oracle.textindex.ClickHouseTextIndexPreprocessorOracle;
2831
import sqlancer.clickhouse.oracle.topk.ClickHouseTopKOracle;
2932
import sqlancer.clickhouse.oracle.partition.ClickHousePartitionMirrorOracle;
3033
import sqlancer.clickhouse.oracle.pqs.ClickHousePivotedQuerySynthesisOracle;
@@ -357,5 +360,26 @@ public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalStat
357360
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
358361
return new ClickHouseStatsToggleOracle(globalState);
359362
}
363+
},
364+
TextIndexPreprocessor {
365+
366+
@Override
367+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
368+
return new ClickHouseTextIndexPreprocessorOracle(globalState);
369+
}
370+
},
371+
TextIndexContainer {
372+
373+
@Override
374+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
375+
return new ClickHouseTextIndexContainerOracle(globalState);
376+
}
377+
},
378+
TextIndexLifecycle {
379+
380+
@Override
381+
public TestOracle<ClickHouseGlobalState> create(ClickHouseGlobalState globalState) throws SQLException {
382+
return new ClickHouseTextIndexLifecycleOracle(globalState);
383+
}
360384
}
361385
}

src/sqlancer/clickhouse/gen/ClickHouseAlterGenerator.java

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.util.List;
44

5+
import com.clickhouse.data.ClickHouseDataType;
6+
57
import sqlancer.IgnoreMeException;
68
import sqlancer.Randomly;
79
import sqlancer.clickhouse.ClickHouseErrors;
@@ -18,7 +20,8 @@ private ClickHouseAlterGenerator() {
1820
}
1921

2022
private enum AlterKind {
21-
ADD_COLUMN, DROP_COLUMN, MODIFY_COLUMN, RENAME_COLUMN, COMMENT_COLUMN, ADD_PROJECTION
23+
ADD_COLUMN, DROP_COLUMN, MODIFY_COLUMN, RENAME_COLUMN, COMMENT_COLUMN, ADD_PROJECTION, ADD_INDEX,
24+
MATERIALIZE_INDEX, CLEAR_INDEX, DROP_INDEX
2225
}
2326

2427
public static SQLQueryAdapter getQuery(ClickHouseGlobalState state) {
@@ -51,12 +54,25 @@ public static SQLQueryAdapter getQuery(ClickHouseGlobalState state) {
5154
case ADD_PROJECTION:
5255
renderAddProjection(sb, table);
5356
break;
57+
case ADD_INDEX:
58+
renderAddIndex(sb, table);
59+
break;
60+
case MATERIALIZE_INDEX:
61+
renderMaterializeIndex(sb, table);
62+
break;
63+
case CLEAR_INDEX:
64+
renderClearIndex(sb, table);
65+
break;
66+
case DROP_INDEX:
67+
renderDropIndex(sb, table);
68+
break;
5469
default:
5570
throw new AssertionError(kind);
5671
}
5772

5873
ExpectedErrors errors = ExpectedErrors.newErrors().with(ClickHouseErrors.getExpectedExpressionErrors())
59-
.with(ClickHouseErrors.getAlterErrors()).build();
74+
.with(ClickHouseErrors.getAlterErrors()).with(ClickHouseErrors.getTextIndexErrors())
75+
.with(ClickHouseErrors.getMutationErrors()).build();
6076

6177
return new SQLQueryAdapter(sb.toString(), errors, true);
6278
}
@@ -128,6 +144,52 @@ private static void renderAddProjection(StringBuilder sb, ClickHouseTable table)
128144
sb.append(")");
129145
}
130146

147+
private static void renderAddIndex(StringBuilder sb, ClickHouseTable table) {
148+
ClickHouseColumn strCol = pickStringColumn(table);
149+
if (strCol != null) {
150+
sb.append(" ADD INDEX IF NOT EXISTS tidx_").append(strCol.getName()).append(" (").append(strCol.getName())
151+
.append(") TYPE text(tokenizer = ").append(pickTokenizer()).append(") GRANULARITY 1");
152+
} else {
153+
ClickHouseColumn col = Randomly.fromList(table.getColumns());
154+
sb.append(" ADD INDEX IF NOT EXISTS sidx_").append(col.getName()).append(" (").append(col.getName())
155+
.append(") TYPE set(100) GRANULARITY 1");
156+
}
157+
}
158+
159+
private static void renderMaterializeIndex(StringBuilder sb, ClickHouseTable table) {
160+
sb.append(" MATERIALIZE INDEX IF EXISTS ").append(pickIndexName(table));
161+
}
162+
163+
private static void renderClearIndex(StringBuilder sb, ClickHouseTable table) {
164+
sb.append(" CLEAR INDEX IF EXISTS ").append(pickIndexName(table));
165+
}
166+
167+
private static void renderDropIndex(StringBuilder sb, ClickHouseTable table) {
168+
sb.append(" DROP INDEX IF EXISTS ").append(pickIndexName(table));
169+
}
170+
171+
private static String pickIndexName(ClickHouseTable table) {
172+
ClickHouseColumn strCol = pickStringColumn(table);
173+
if (strCol != null) {
174+
return "tidx_" + strCol.getName();
175+
}
176+
return "sidx_" + Randomly.fromList(table.getColumns()).getName();
177+
}
178+
179+
private static ClickHouseColumn pickStringColumn(ClickHouseTable table) {
180+
List<ClickHouseColumn> strCols = new java.util.ArrayList<>();
181+
for (ClickHouseColumn col : table.getColumns()) {
182+
if (col.getType().getType() == ClickHouseDataType.String) {
183+
strCols.add(col);
184+
}
185+
}
186+
return strCols.isEmpty() ? null : Randomly.fromList(strCols);
187+
}
188+
189+
private static String pickTokenizer() {
190+
return Randomly.fromOptions("'splitByNonAlpha'", "ngrams(3)", "'array'", "splitByString([' '])");
191+
}
192+
131193
private static void renderCommentColumn(StringBuilder sb, ClickHouseTable table) {
132194
ClickHouseColumn col = Randomly.fromList(table.getColumns());
133195

src/sqlancer/clickhouse/gen/ClickHouseExpressionGenerator.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,38 @@ public ClickHouseExpression generateStringCall(List<ClickHouseColumnReference> c
423423
return new sqlancer.clickhouse.ast.ClickHouseRawText(sql);
424424
}
425425

426+
private static final List<String> TEXT_SEARCH_VOCABULARY = List.of("alpha", "bravo", "charlie", "delta", "echo",
427+
"foxtrot", "golf", "hotel", "india", "juliet", "clickhouse", "olap", "search", "token", "index", "query");
428+
429+
public ClickHouseExpression generateTextSearchPredicate(List<ClickHouseColumnReference> columns) {
430+
List<ClickHouseColumnReference> stringCols = new java.util.ArrayList<>();
431+
for (ClickHouseColumnReference c : columns) {
432+
if (c.getColumn().getType().getType() == ClickHouseDataType.String) {
433+
stringCols.add(c);
434+
}
435+
}
436+
if (stringCols.isEmpty()) {
437+
return null;
438+
}
439+
String s = ClickHouseToStringVisitor.asString(Randomly.fromList(stringCols));
440+
String w1 = TEXT_SEARCH_VOCABULARY.get((int) Randomly.getNotCachedInteger(0, TEXT_SEARCH_VOCABULARY.size()));
441+
String w2 = TEXT_SEARCH_VOCABULARY.get((int) Randomly.getNotCachedInteger(0, TEXT_SEARCH_VOCABULARY.size()));
442+
String fn = Randomly.fromOptions("startsWith", "endsWith", "multiSearchAny");
443+
String sql;
444+
switch (fn) {
445+
case "startsWith":
446+
sql = "startsWith(" + s + ", '" + w1 + "')";
447+
break;
448+
case "endsWith":
449+
sql = "endsWith(" + s + ", '" + w1 + "')";
450+
break;
451+
default:
452+
sql = "multiSearchAny(" + s + ", ['" + w1 + "', '" + w2 + "'])";
453+
break;
454+
}
455+
return new sqlancer.clickhouse.ast.ClickHouseRawText(sql);
456+
}
457+
426458
public ClickHouseExpression generateDateTransform(List<ClickHouseColumnReference> columns) {
427459
List<ClickHouseColumnReference> dateCols = new java.util.ArrayList<>();
428460
boolean dateTimeResolution = false;
@@ -1307,6 +1339,16 @@ public ClickHouseExpression generatePredicate() {
13071339
}
13081340
}
13091341

1342+
if (globalState.getClickHouseOptions().textSearchPredicateEmission
1343+
&& Randomly.getBooleanWithSmallProbability()) {
1344+
ClickHouseExpression textPred = generateTextSearchPredicate(columnRefs);
1345+
if (textPred != null) {
1346+
return Randomly.getBoolean() ? textPred
1347+
: new ClickHouseBinaryLogicalOperation(base, textPred,
1348+
ClickHouseBinaryLogicalOperation.ClickHouseBinaryLogicalOperator.AND);
1349+
}
1350+
}
1351+
13101352
if (ClickHouseVariantPredicateFactory.gateOpen(globalState.getClickHouseOptions().variantWhereEmission,
13111353
Randomly.getBooleanWithSmallProbability())) {
13121354
List<String> intExprs = integerColumns(columnRefs).stream()

src/sqlancer/clickhouse/gen/ClickHouseTableGenerator.java

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public static SQLQueryAdapter createTableStatement(String tableName,
5252
chTableGenerator.start();
5353
ExpectedErrors errors = new ExpectedErrors();
5454
ClickHouseErrors.addExpectedExpressionErrors(errors);
55+
ClickHouseErrors.addTextIndexErrors(errors);
5556
return new SQLQueryAdapter(chTableGenerator.sb.toString(), errors, true);
5657
}
5758

@@ -399,25 +400,63 @@ private String renderProjection(int idx, List<ClickHouseSchema.ClickHouseColumn>
399400

400401
private String renderSkipIndex(int idx, ClickHouseSchema.ClickHouseColumn col) {
401402
ClickHouseDataType t = col.getType().getType();
403+
String textTarget = textIndexTarget(col);
402404

403-
List<String> typeChoices = new ArrayList<>();
404-
typeChoices.add("bloom_filter(0.01)");
405-
typeChoices.add("set(100)");
405+
List<String[]> candidates = new ArrayList<>();
406+
candidates.add(new String[] { col.getName(), "bloom_filter(0.01)" });
407+
candidates.add(new String[] { col.getName(), "set(100)" });
406408
if (t == ClickHouseDataType.Int32 || t == ClickHouseDataType.String) {
407-
typeChoices.add("minmax");
409+
candidates.add(new String[] { col.getName(), "minmax" });
408410
}
409411
if (t == ClickHouseDataType.String) {
410-
typeChoices.add("ngrambf_v1(3, 256, 2, 0)");
411-
412-
typeChoices.add("text(tokenizer = 'splitByNonAlpha')");
413-
typeChoices.add("text(tokenizer = ngrams(3))");
412+
candidates.add(new String[] { col.getName(), "ngrambf_v1(3, 256, 2, 0)" });
413+
}
414+
if (textTarget != null) {
415+
candidates.add(new String[] { textTarget, "text(tokenizer = " + pickTokenizer() + ")" });
416+
candidates.add(new String[] { textTarget, "text(tokenizer = " + pickTokenizer() + ")" });
414417
}
415-
String type = Randomly.fromList(typeChoices);
416-
int granularity = Randomly.fromOptions(1, 2, 4);
417-
return String.format("INDEX idx_%s_%d %s TYPE %s GRANULARITY %d", col.getName(), idx, col.getName(), type,
418+
419+
String[] chosen = Randomly.fromList(candidates);
420+
int granularity = chosen[1].startsWith("text(") ? 1 : Randomly.fromOptions(1, 2, 4);
421+
return String.format("INDEX idx_%s_%d %s TYPE %s GRANULARITY %d", col.getName(), idx, chosen[0], chosen[1],
418422
granularity);
419423
}
420424

425+
private static String pickTokenizer() {
426+
return Randomly.fromOptions("'splitByNonAlpha'", "ngrams(2)", "ngrams(3)", "ngrams(4)", "'array'", "'asciiCJK'",
427+
"splitByString([' '])", "splitByString([' ', '-', '::'])", "sparseGrams(3, 5)");
428+
}
429+
430+
private static String textIndexTarget(ClickHouseSchema.ClickHouseColumn col) {
431+
sqlancer.clickhouse.ClickHouseType u = col.getType().getTypeTerm().unwrap();
432+
if (isStringLeaf(u)) {
433+
return col.getName();
434+
}
435+
if (u instanceof sqlancer.clickhouse.ClickHouseType.Array arr && isStringLeaf(arr.inner().unwrap())) {
436+
return col.getName();
437+
}
438+
if (u instanceof sqlancer.clickhouse.ClickHouseType.Map m) {
439+
boolean keyStr = isStringLeaf(m.keyType().unwrap());
440+
boolean valStr = isStringLeaf(m.valueType().unwrap());
441+
if (keyStr && valStr) {
442+
return Randomly.getBoolean() ? "mapKeys(" + col.getName() + ")" : "mapValues(" + col.getName() + ")";
443+
}
444+
if (keyStr) {
445+
return "mapKeys(" + col.getName() + ")";
446+
}
447+
if (valStr) {
448+
return "mapValues(" + col.getName() + ")";
449+
}
450+
}
451+
return null;
452+
}
453+
454+
private static boolean isStringLeaf(sqlancer.clickhouse.ClickHouseType t) {
455+
return t instanceof sqlancer.clickhouse.ClickHouseType.Primitive p
456+
&& p.kind() == sqlancer.clickhouse.ClickHouseType.Kind.String
457+
|| t instanceof sqlancer.clickhouse.ClickHouseType.FixedString;
458+
}
459+
421460
private static final int CLAUSE_VALIDATION_RETRY_LIMIT = 5;
422461

423462
private static ClickHouseExpression generateValidated(Supplier<ClickHouseExpression> factory,

0 commit comments

Comments
 (0)