Skip to content

Commit 2bdf043

Browse files
authored
Merge pull request #1360 from sqlancer/feature/query-reduction
Implement query reduction for EET SELECT
2 parents e854475 + 79e3ca0 commit 2bdf043

6 files changed

Lines changed: 597 additions & 52 deletions

File tree

src/sqlancer/Main.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ public void setReductionContext(List<Query<?>> setupStatements, String bugInform
270270
this.reduceBugInformation = bugInformation;
271271
}
272272

273+
// for reducers that rewrite the failing queries themselves (e.g., TransformationReducer)
274+
public void updateReducedBugInformation(String bugInformation) {
275+
this.reduceBugInformation = bugInformation;
276+
}
277+
273278
public void logReduced(StateToReproduce state) {
274279
nrReductionAttempts++;
275280
logReduced(state, "Reduction attempt " + nrReductionAttempts
@@ -525,6 +530,11 @@ public void run() throws Exception {
525530
astBasedReducer.reduce(state, reproducer, newGlobalState);
526531
}
527532

533+
// reduces the oracle's transformed query itself; a no-op for reproducers whose queries are not
534+
// built from reducible transformations
535+
Reducer<G> transformationReducer = new TransformationReducer<>(provider);
536+
transformationReducer.reduce(state, reproducer, newGlobalState);
537+
528538
// reassemble the statements so that the main log looks like one produced
529539
// without the reducer, with the generation statements replaced by the reduced
530540
// ones and the oracle queries at the end

src/sqlancer/Randomly.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,29 @@ public static double getUncachedDouble() {
510510
return getThreadRandom().get().nextDouble();
511511
}
512512

513+
/**
514+
* Computes {@code value} with this thread's random number generator temporarily replaced by a fixed-seed one,
515+
* restoring the previous generator afterwards. This makes computations that draw randomness deterministic: for
516+
* example, rendering an AST to SQL draws random textual variants in some DBMS implementations, and test-case
517+
* reduction relies on re-rendering the same AST to the same string.
518+
*
519+
* @param <T>
520+
* the type of the computed value
521+
* @param value
522+
* the computation to run deterministically
523+
*
524+
* @return the computed value
525+
*/
526+
public static <T> T withFixedSeedRandom(Supplier<T> value) {
527+
Random previousRandom = THREAD_RANDOM.get();
528+
THREAD_RANDOM.set(new Random(0));
529+
try {
530+
return value.get();
531+
} finally {
532+
THREAD_RANDOM.set(previousRandom);
533+
}
534+
}
535+
513536
public String getChar() {
514537
while (true) {
515538
String s = getString();
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package sqlancer;
2+
3+
import java.time.Duration;
4+
import java.time.Instant;
5+
import java.util.ArrayList;
6+
import java.util.HashSet;
7+
import java.util.List;
8+
9+
import sqlancer.common.query.Query;
10+
11+
/**
12+
* Reduces the transformed query of a {@link TransformationReproducer} by disabling transformation sites, searching
13+
* (with the same delta-debugging strategy as {@link StatementReducer}) for a minimal set of sites that still triggers
14+
* the bug. Because each site is an individually equivalence-preserving rewrite, any subset of sites yields a
15+
* transformed query that is still semantically equivalent to the original query, so the reduction is sound. This
16+
* reducer runs after statement reduction, evaluating each candidate against the already-reduced database; for
17+
* reproducers that do not implement {@link TransformationReproducer}, it does nothing.
18+
*
19+
* @param <G>
20+
* the DBMS-specific global state class
21+
* @param <O>
22+
* the DBMS-specific options class
23+
* @param <C>
24+
* the DBMS-specific connection class
25+
*/
26+
public class TransformationReducer<G extends GlobalState<O, ?, C>, O extends DBMSSpecificOptions<?>, C extends SQLancerDBConnection>
27+
implements Reducer<G> {
28+
29+
private final DatabaseProvider<G, O, C> provider;
30+
private List<Query<C>> statements;
31+
private boolean observedChange;
32+
private int partitionNum;
33+
34+
private long currentReduceSteps;
35+
private long currentReduceTime;
36+
37+
private long maxReduceSteps;
38+
private long maxReduceTime;
39+
40+
private Instant timeOfReductionBegins;
41+
42+
public TransformationReducer(DatabaseProvider<G, O, C> provider) {
43+
this.provider = provider;
44+
}
45+
46+
private boolean hasNotReachedLimit(long curr, long limit) {
47+
if (limit == MainOptions.NO_REDUCE_LIMIT) {
48+
return true;
49+
}
50+
return curr < limit;
51+
}
52+
53+
@SuppressWarnings("unchecked")
54+
@Override
55+
public void reduce(G state, Reproducer<G> reproducer, G newGlobalState) throws Exception {
56+
if (!(reproducer instanceof TransformationReproducer)) {
57+
return;
58+
}
59+
TransformationReproducer<G> transformationReproducer = (TransformationReproducer<G>) reproducer;
60+
61+
maxReduceTime = state.getOptions().getMaxStatementReduceTime();
62+
maxReduceSteps = state.getOptions().getMaxStatementReduceSteps();
63+
64+
// Snapshot the (already reduced) generation statements once: createDatabase logs its setup statements
65+
// (DROP/CREATE/USE) into the state, so the state's statement list must be reset for every candidate rather
66+
// than read back, lest the setup statements accumulate and get re-executed mid-test-case.
67+
statements = new ArrayList<>();
68+
for (Query<?> stat : newGlobalState.getState().getStatements()) {
69+
statements.add((Query<C>) stat);
70+
}
71+
72+
List<Integer> enabledSites = new ArrayList<>();
73+
for (int site = 0; site < transformationReproducer.getTransformationSiteCount(); site++) {
74+
enabledSites.add(site);
75+
}
76+
// With every site disabled the transformed query renders as the original one, which cannot mismatch with
77+
// itself, so a single remaining site cannot be reduced further.
78+
if (enabledSites.size() < 2) {
79+
return;
80+
}
81+
82+
timeOfReductionBegins = Instant.now();
83+
currentReduceSteps = 0;
84+
currentReduceTime = 0;
85+
partitionNum = 2;
86+
87+
while (enabledSites.size() >= 2 && hasNotReachedLimit(currentReduceSteps, maxReduceSteps)
88+
&& hasNotReachedLimit(currentReduceTime, maxReduceTime)) {
89+
observedChange = false;
90+
91+
enabledSites = tryReduction(transformationReproducer, newGlobalState, enabledSites);
92+
93+
if (!observedChange) {
94+
if (partitionNum == enabledSites.size()) {
95+
break;
96+
}
97+
// increase the search granularity
98+
partitionNum = Math.min(partitionNum * 2, enabledSites.size());
99+
}
100+
}
101+
102+
// Leave the reproducer holding the reduced transformed query (the last candidate tried may have failed), so
103+
// the final bug information reflects the reduction.
104+
transformationReproducer.setEnabledTransformationSites(new HashSet<>(enabledSites));
105+
newGlobalState.getState().setStatements(new ArrayList<>(statements));
106+
newGlobalState.getLogger().updateReducedBugInformation(transformationReproducer.getBugInformation());
107+
newGlobalState.getLogger().logReduced(newGlobalState.getState(),
108+
"Transformation reduction finished; the transformed query was reduced to the one shown below");
109+
}
110+
111+
private List<Integer> tryReduction(TransformationReproducer<G> transformationReproducer, G newGlobalState,
112+
List<Integer> enabledSites) throws Exception {
113+
114+
List<Integer> sites = enabledSites;
115+
116+
int start = 0;
117+
int subLength = sites.size() / partitionNum;
118+
while (start < sites.size()) {
119+
// candidateSites = sites[:start] + sites[start+subLength:]
120+
// in other words, remove [start, start+subLength) from sites
121+
List<Integer> candidateSites = new ArrayList<>(sites);
122+
int endPoint = Math.min(start + subLength, candidateSites.size());
123+
candidateSites.subList(start, endPoint).clear();
124+
125+
if (bugStillTriggersWith(transformationReproducer, newGlobalState, candidateSites)) {
126+
observedChange = true;
127+
sites = candidateSites;
128+
partitionNum = Math.max(partitionNum - 1, 2);
129+
newGlobalState.getLogger().updateReducedBugInformation(transformationReproducer.getBugInformation());
130+
newGlobalState.getLogger().logReduced(newGlobalState.getState());
131+
break;
132+
}
133+
134+
currentReduceSteps++;
135+
currentReduceTime = Duration.between(timeOfReductionBegins, Instant.now()).getSeconds();
136+
if (!hasNotReachedLimit(currentReduceSteps, maxReduceSteps)
137+
|| !hasNotReachedLimit(currentReduceTime, maxReduceTime)) {
138+
return sites;
139+
}
140+
start = start + subLength;
141+
}
142+
return sites;
143+
}
144+
145+
/**
146+
* Whether the bug still triggers with only {@code candidateSites} applied to the transformed query, evaluated
147+
* against a freshly recreated database populated with the (already reduced) generation statements.
148+
*
149+
* @param transformationReproducer
150+
* the reproducer whose transformed query is being reduced
151+
* @param newGlobalState
152+
* the state the candidate is evaluated against
153+
* @param candidateSites
154+
* the transformation sites to keep applied
155+
*
156+
* @return {@code true} if the bug still triggers with the candidate sites
157+
*/
158+
private boolean bugStillTriggersWith(TransformationReproducer<G> transformationReproducer, G newGlobalState,
159+
List<Integer> candidateSites) {
160+
transformationReproducer.setEnabledTransformationSites(new HashSet<>(candidateSites));
161+
try (C con2 = provider.createDatabase(newGlobalState)) {
162+
newGlobalState.setConnection(con2);
163+
// discard the setup statements createDatabase just logged into the state
164+
newGlobalState.getState().setStatements(new ArrayList<>(statements));
165+
for (Query<C> s : statements) {
166+
try {
167+
s.execute(newGlobalState);
168+
} catch (Throwable ignoredException) {
169+
// ignore
170+
}
171+
}
172+
try {
173+
return transformationReproducer.bugStillTriggers(newGlobalState);
174+
} catch (Throwable ignoredException) {
175+
// fall through: this candidate no longer triggers the bug
176+
}
177+
} catch (Exception e) {
178+
e.printStackTrace();
179+
}
180+
return false;
181+
}
182+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package sqlancer;
2+
3+
import java.util.Set;
4+
5+
/**
6+
* A {@link Reproducer} for bugs found by comparing an original query against a transformed one, where the transformed
7+
* query was built by applying individually equivalence-preserving transformations (e.g. the EET rules) to the original.
8+
* Each such application is a transformation site, identified by an index that stays stable no matter which sites are
9+
* enabled. Disabling any subset of sites re-renders a transformed query that is still semantically equivalent to the
10+
* original, so {@link TransformationReducer} can soundly search for a minimal set of sites that still triggers the bug.
11+
*
12+
* @param <G>
13+
* the DBMS-specific global state class
14+
*/
15+
public interface TransformationReproducer<G extends GlobalState<?, ?, ?>> extends Reproducer<G> {
16+
17+
/**
18+
* The total number of transformation sites the transformed query was built with. This does not change when sites
19+
* are disabled.
20+
*
21+
* @return the total number of transformation sites
22+
*/
23+
int getTransformationSiteCount();
24+
25+
/**
26+
* Re-renders the transformed query with only the given transformation sites applied. Later
27+
* {@link #bugStillTriggers} calls and {@link #getBugInformation} use the re-rendered query.
28+
*
29+
* @param enabledSites
30+
* the indices ({@code 0} to {@code getTransformationSiteCount() - 1}) of the sites to keep applied
31+
*/
32+
void setEnabledTransformationSites(Set<Integer> enabledSites);
33+
}

src/sqlancer/common/oracle/EETOracle.java

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package sqlancer.common.oracle;
22

33
import java.sql.SQLException;
4+
import java.util.ArrayList;
45
import java.util.List;
5-
import java.util.stream.Collectors;
6+
import java.util.Set;
67

78
import sqlancer.ComparatorHelper;
9+
import sqlancer.Randomly;
810
import sqlancer.Reproducer;
911
import sqlancer.SQLGlobalState;
12+
import sqlancer.TransformationReproducer;
1013
import sqlancer.common.ast.newast.Expression;
1114
import sqlancer.common.ast.newast.Join;
1215
import sqlancer.common.ast.newast.Select;
@@ -53,13 +56,72 @@ public class EETOracle<Z extends Select<J, E, T, C>, J extends Join<E, T, C>, E
5356
private Reproducer<G> reproducer;
5457
private String generatedQueryString;
5558

56-
private final class EETReproducer extends AbstractComparisonReproducer<G, List<String>> {
59+
private final class EETReproducer extends AbstractComparisonReproducer<G, List<String>>
60+
implements TransformationReproducer<G> {
5761
private final String originalQueryString;
58-
private final String transformedQueryString;
62+
// Mutable: transformation reduction re-renders the transformed query with some transformation sites disabled.
63+
private String transformedQueryString;
64+
private final String initialTransformedQueryString;
5965

60-
EETReproducer(String originalQueryString, String transformedQueryString) {
66+
// The query parts needed to re-render the transformed query: the SELECT whose fetch columns and WHERE clause
67+
// are replaced, the untransformed expressions, and the records of their transformations.
68+
private final Z select;
69+
private final List<E> fetchColumns;
70+
private final List<EETTransformer.TransformationRecord> fetchColumnRecords;
71+
private final E whereClause;
72+
private final EETTransformer.TransformationRecord whereClauseRecord;
73+
74+
EETReproducer(String originalQueryString, String transformedQueryString, Z select, List<E> fetchColumns,
75+
List<EETTransformer.TransformationRecord> fetchColumnRecords, E whereClause,
76+
EETTransformer.TransformationRecord whereClauseRecord) {
6177
this.originalQueryString = originalQueryString;
6278
this.transformedQueryString = transformedQueryString;
79+
this.initialTransformedQueryString = transformedQueryString;
80+
this.select = select;
81+
this.fetchColumns = fetchColumns;
82+
this.fetchColumnRecords = fetchColumnRecords;
83+
this.whereClause = whereClause;
84+
this.whereClauseRecord = whereClauseRecord;
85+
}
86+
87+
@Override
88+
public int getTransformationSiteCount() {
89+
int siteCount = whereClauseRecord.getSiteCount();
90+
for (EETTransformer.TransformationRecord record : fetchColumnRecords) {
91+
siteCount += record.getSiteCount();
92+
}
93+
return siteCount;
94+
}
95+
96+
@Override
97+
public void setEnabledTransformationSites(Set<Integer> enabledSites) {
98+
if (enabledSites.size() == getTransformationSiteCount()) {
99+
// With every site enabled, the transformed query is the unreduced one; keep the exact string that
100+
// originally detected the bug rather than re-rendering it (rendering an AST draws random textual
101+
// variants, so a re-render would produce a semantically equal but untested string).
102+
transformedQueryString = initialTransformedQueryString;
103+
return;
104+
}
105+
// Pin the RNG while re-rendering so the same enabled sites always yield the same query string; the string
106+
// tested during reduction is then exactly the string the reduced test case reports.
107+
transformedQueryString = Randomly.withFixedSeedRandom(() -> {
108+
// Global site indices are assigned over the fetch columns' records first (in column order), then the
109+
// WHERE clause's record.
110+
List<E> replayedFetchColumns = new ArrayList<>();
111+
int offset = 0;
112+
for (int i = 0; i < fetchColumns.size(); i++) {
113+
int base = offset;
114+
replayedFetchColumns.add(transformer.replay(fetchColumns.get(i), false, fetchColumnRecords.get(i),
115+
site -> enabledSites.contains(base + site)));
116+
offset += fetchColumnRecords.get(i).getSiteCount();
117+
}
118+
int whereBase = offset;
119+
E replayedWhereClause = transformer.replay(whereClause, true, whereClauseRecord,
120+
site -> enabledSites.contains(whereBase + site));
121+
select.setFetchColumns(replayedFetchColumns);
122+
select.setWhereClause(replayedWhereClause);
123+
return select.asString();
124+
});
63125
}
64126

65127
@Override
@@ -160,11 +222,17 @@ public void check() throws SQLException {
160222
}
161223

162224
// Transform the query's expressions into semantically equivalent ones. Fetch columns are scalar expressions,
163-
// while the WHERE clause is evaluated in a boolean context.
164-
List<E> transformedFetchColumns = fetchColumns.stream().map(c -> transformer.transform(c, false))
165-
.collect(Collectors.toList());
225+
// while the WHERE clause is evaluated in a boolean context. Each transformation's record is kept so the
226+
// reproducer can replay it with transformation sites disabled during reduction.
227+
List<E> transformedFetchColumns = new ArrayList<>();
228+
List<EETTransformer.TransformationRecord> fetchColumnRecords = new ArrayList<>();
229+
for (E fetchColumn : fetchColumns) {
230+
transformedFetchColumns.add(transformer.transform(fetchColumn, false));
231+
fetchColumnRecords.add(transformer.getLastTransformationRecord());
232+
}
166233
select.setFetchColumns(transformedFetchColumns);
167234
select.setWhereClause(transformer.transform(whereClause, true));
235+
EETTransformer.TransformationRecord whereClauseRecord = transformer.getLastTransformationRecord();
168236

169237
String transformedQueryString = select.asString();
170238
List<String> transformedResultSet;
@@ -181,7 +249,8 @@ public void check() throws SQLException {
181249

182250
// Set the reproducer before the assertion: assumeResultSetsAreEqual throws when the bug is
183251
// detected, so creating the reproducer afterwards would leave it null and prevent any reduction.
184-
reproducer = new EETReproducer(originalQueryString, transformedQueryString);
252+
reproducer = new EETReproducer(originalQueryString, transformedQueryString, select, fetchColumns,
253+
fetchColumnRecords, whereClause, whereClauseRecord);
185254

186255
ComparatorHelper.assumeResultSetsAreEqual(originalResultSet, transformedResultSet, originalQueryString,
187256
List.of(transformedQueryString), state);

0 commit comments

Comments
 (0)