Skip to content

Commit 9eb1db8

Browse files
authored
Merge pull request #1361 from sqlancer/feature/query-reduction
Add per-site simplification to EET transformed query reduction
2 parents 2bdf043 + eafc978 commit 9eb1db8

5 files changed

Lines changed: 310 additions & 72 deletions

File tree

src/sqlancer/TransformationReducer.java

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@
55
import java.util.ArrayList;
66
import java.util.HashSet;
77
import java.util.List;
8+
import java.util.Set;
89

910
import sqlancer.common.query.Query;
1011

1112
/**
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.
13+
* Reduces the transformed query of a {@link TransformationReproducer} in two phases. First, transformation sites are
14+
* disabled with the same delta-debugging strategy as {@link StatementReducer}, searching for a minimal set of sites
15+
* that still triggers the bug. Second, each surviving site is greedily simplified: its always-true (or always-false)
16+
* condition is rendered as a literal constant, and its generated dead branch is replaced by a copy of the live
17+
* expression, keeping each simplification only if the bug still triggers. Because each site is an individually
18+
* equivalence-preserving rewrite and both simplifications preserve that property, every candidate transformed query
19+
* remains semantically equivalent to the original query, so the reduction is sound. This reducer runs after statement
20+
* reduction, evaluating each candidate against the already-reduced database; for reproducers that do not implement
21+
* {@link TransformationReproducer}, it does nothing.
1822
*
1923
* @param <G>
2024
* the DBMS-specific global state class
@@ -39,6 +43,9 @@ public class TransformationReducer<G extends GlobalState<O, ?, C>, O extends DBM
3943

4044
private Instant timeOfReductionBegins;
4145

46+
private Set<Integer> constantConditionSites;
47+
private Set<Integer> copiedDeadBranchSites;
48+
4249
public TransformationReducer(DatabaseProvider<G, O, C> provider) {
4350
this.provider = provider;
4451
}
@@ -50,6 +57,18 @@ private boolean hasNotReachedLimit(long curr, long limit) {
5057
return curr < limit;
5158
}
5259

60+
private boolean withinLimits() {
61+
return hasNotReachedLimit(currentReduceSteps, maxReduceSteps)
62+
&& hasNotReachedLimit(currentReduceTime, maxReduceTime);
63+
}
64+
65+
// Accounts one candidate evaluation against the step/time limits; returns whether reduction may continue.
66+
private boolean registerStepAndCheckLimits() {
67+
currentReduceSteps++;
68+
currentReduceTime = Duration.between(timeOfReductionBegins, Instant.now()).getSeconds();
69+
return withinLimits();
70+
}
71+
5372
@SuppressWarnings("unchecked")
5473
@Override
5574
public void reduce(G state, Reproducer<G> reproducer, G newGlobalState) throws Exception {
@@ -73,19 +92,20 @@ public void reduce(G state, Reproducer<G> reproducer, G newGlobalState) throws E
7392
for (int site = 0; site < transformationReproducer.getTransformationSiteCount(); site++) {
7493
enabledSites.add(site);
7594
}
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) {
95+
if (enabledSites.isEmpty()) {
7996
return;
8097
}
8198

8299
timeOfReductionBegins = Instant.now();
83100
currentReduceSteps = 0;
84101
currentReduceTime = 0;
85102
partitionNum = 2;
103+
constantConditionSites = new HashSet<>();
104+
copiedDeadBranchSites = new HashSet<>();
86105

87-
while (enabledSites.size() >= 2 && hasNotReachedLimit(currentReduceSteps, maxReduceSteps)
88-
&& hasNotReachedLimit(currentReduceTime, maxReduceTime)) {
106+
// Phase 1: delta-debug the enabled-site set. With every site disabled the transformed query renders as the
107+
// original one, which cannot mismatch with itself, so a single remaining site is not removable further.
108+
while (enabledSites.size() >= 2 && withinLimits()) {
89109
observedChange = false;
90110

91111
enabledSites = tryReduction(transformationReproducer, newGlobalState, enabledSites);
@@ -99,9 +119,12 @@ && hasNotReachedLimit(currentReduceTime, maxReduceTime)) {
99119
}
100120
}
101121

122+
simplifySurvivingSites(transformationReproducer, newGlobalState, enabledSites);
123+
102124
// Leave the reproducer holding the reduced transformed query (the last candidate tried may have failed), so
103125
// the final bug information reflects the reduction.
104-
transformationReproducer.setEnabledTransformationSites(new HashSet<>(enabledSites));
126+
transformationReproducer.applyTransformationSites(new HashSet<>(enabledSites), constantConditionSites,
127+
copiedDeadBranchSites);
105128
newGlobalState.getState().setStatements(new ArrayList<>(statements));
106129
newGlobalState.getLogger().updateReducedBugInformation(transformationReproducer.getBugInformation());
107130
newGlobalState.getLogger().logReduced(newGlobalState.getState(),
@@ -126,15 +149,11 @@ private List<Integer> tryReduction(TransformationReproducer<G> transformationRep
126149
observedChange = true;
127150
sites = candidateSites;
128151
partitionNum = Math.max(partitionNum - 1, 2);
129-
newGlobalState.getLogger().updateReducedBugInformation(transformationReproducer.getBugInformation());
130-
newGlobalState.getLogger().logReduced(newGlobalState.getState());
152+
logReductionStep(transformationReproducer, newGlobalState);
131153
break;
132154
}
133155

134-
currentReduceSteps++;
135-
currentReduceTime = Duration.between(timeOfReductionBegins, Instant.now()).getSeconds();
136-
if (!hasNotReachedLimit(currentReduceSteps, maxReduceSteps)
137-
|| !hasNotReachedLimit(currentReduceTime, maxReduceTime)) {
156+
if (!registerStepAndCheckLimits()) {
138157
return sites;
139158
}
140159
start = start + subLength;
@@ -143,8 +162,59 @@ private List<Integer> tryReduction(TransformationReproducer<G> transformationRep
143162
}
144163

145164
/**
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.
165+
* Phase 2: greedily simplifies each surviving site, keeping a simplification only if the bug still triggers. First
166+
* the site's condition is rendered as a literal constant (the condition's embedded random predicate is often the
167+
* bulk of the transformed query), then, for sites that have one, the generated dead branch is replaced by a copy of
168+
* the live expression.
169+
*
170+
* @param transformationReproducer
171+
* the reproducer whose transformed query is being reduced
172+
* @param newGlobalState
173+
* the state the candidates are evaluated against
174+
* @param enabledSites
175+
* the sites that survived phase 1
176+
*/
177+
private void simplifySurvivingSites(TransformationReproducer<G> transformationReproducer, G newGlobalState,
178+
List<Integer> enabledSites) {
179+
Set<Integer> deadBranchSites = transformationReproducer.getDeadBranchSites();
180+
for (int site : enabledSites) {
181+
if (!withinLimits()) {
182+
return;
183+
}
184+
constantConditionSites.add(site);
185+
if (bugStillTriggersWith(transformationReproducer, newGlobalState, enabledSites)) {
186+
logReductionStep(transformationReproducer, newGlobalState);
187+
} else {
188+
constantConditionSites.remove(site);
189+
}
190+
if (!registerStepAndCheckLimits()) {
191+
return;
192+
}
193+
194+
if (deadBranchSites.contains(site)) {
195+
copiedDeadBranchSites.add(site);
196+
if (bugStillTriggersWith(transformationReproducer, newGlobalState, enabledSites)) {
197+
logReductionStep(transformationReproducer, newGlobalState);
198+
} else {
199+
copiedDeadBranchSites.remove(site);
200+
}
201+
if (!registerStepAndCheckLimits()) {
202+
return;
203+
}
204+
}
205+
}
206+
}
207+
208+
// Logs an accepted reduction step, refreshing the logged bug information with the re-rendered transformed query.
209+
private void logReductionStep(TransformationReproducer<G> transformationReproducer, G newGlobalState) {
210+
newGlobalState.getLogger().updateReducedBugInformation(transformationReproducer.getBugInformation());
211+
newGlobalState.getLogger().logReduced(newGlobalState.getState());
212+
}
213+
214+
/**
215+
* Whether the bug still triggers with the given sites applied to the transformed query (further simplified per the
216+
* current constant-condition and copied-dead-branch sets), evaluated against a freshly recreated database populated
217+
* with the (already reduced) generation statements.
148218
*
149219
* @param transformationReproducer
150220
* the reproducer whose transformed query is being reduced
@@ -153,11 +223,12 @@ private List<Integer> tryReduction(TransformationReproducer<G> transformationRep
153223
* @param candidateSites
154224
* the transformation sites to keep applied
155225
*
156-
* @return {@code true} if the bug still triggers with the candidate sites
226+
* @return {@code true} if the bug still triggers with the candidate configuration
157227
*/
158228
private boolean bugStillTriggersWith(TransformationReproducer<G> transformationReproducer, G newGlobalState,
159229
List<Integer> candidateSites) {
160-
transformationReproducer.setEnabledTransformationSites(new HashSet<>(candidateSites));
230+
transformationReproducer.applyTransformationSites(new HashSet<>(candidateSites), constantConditionSites,
231+
copiedDeadBranchSites);
161232
try (C con2 = provider.createDatabase(newGlobalState)) {
162233
newGlobalState.setConnection(con2);
163234
// discard the setup statements createDatabase just logged into the state

src/sqlancer/TransformationReproducer.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,28 @@ public interface TransformationReproducer<G extends GlobalState<?, ?, ?>> extend
2323
int getTransformationSiteCount();
2424

2525
/**
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.
26+
* The transformation sites whose rule application embeds a generated dead-branch expression, which
27+
* {@link #applyTransformationSites} may replace with a copy of the live expression.
28+
*
29+
* @return the indices of the sites with a generated dead branch
30+
*/
31+
Set<Integer> getDeadBranchSites();
32+
33+
/**
34+
* Re-renders the transformed query with only the given transformation sites applied, further simplified per site: a
35+
* site in {@code constantConditionSites} renders its always-true (or always-false) condition as the literal
36+
* constant of the same truth value, and a site in {@code copiedDeadBranchSites} replaces its generated dead branch
37+
* with a copy of the live expression. Both simplifications preserve the equivalence of the transformed query, like
38+
* disabling a site does. Later {@link #bugStillTriggers} calls and {@link #getBugInformation} use the re-rendered
39+
* query.
2840
*
2941
* @param enabledSites
3042
* the indices ({@code 0} to {@code getTransformationSiteCount() - 1}) of the sites to keep applied
43+
* @param constantConditionSites
44+
* the indices of the enabled sites whose condition is rendered as a literal constant
45+
* @param copiedDeadBranchSites
46+
* the indices of the enabled sites whose dead branch is replaced by a copy of the live expression
3147
*/
32-
void setEnabledTransformationSites(Set<Integer> enabledSites);
48+
void applyTransformationSites(Set<Integer> enabledSites, Set<Integer> constantConditionSites,
49+
Set<Integer> copiedDeadBranchSites);
3350
}

src/sqlancer/common/oracle/EETOracle.java

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.sql.SQLException;
44
import java.util.ArrayList;
5+
import java.util.HashSet;
56
import java.util.List;
67
import java.util.Set;
78

@@ -94,36 +95,75 @@ public int getTransformationSiteCount() {
9495
}
9596

9697
@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
98+
public Set<Integer> getDeadBranchSites() {
99+
// Global site indices are assigned over the fetch columns' records first (in column order), then the
100+
// WHERE clause's record.
101+
Set<Integer> deadBranchSites = new HashSet<>();
102+
int offset = 0;
103+
for (EETTransformer.TransformationRecord record : fetchColumnRecords) {
104+
for (int site : record.getDeadBranchSites()) {
105+
deadBranchSites.add(offset + site);
106+
}
107+
offset += record.getSiteCount();
108+
}
109+
for (int site : whereClauseRecord.getDeadBranchSites()) {
110+
deadBranchSites.add(offset + site);
111+
}
112+
return deadBranchSites;
113+
}
114+
115+
@Override
116+
public void applyTransformationSites(Set<Integer> enabledSites, Set<Integer> constantConditionSites,
117+
Set<Integer> copiedDeadBranchSites) {
118+
if (enabledSites.size() == getTransformationSiteCount() && constantConditionSites.isEmpty()
119+
&& copiedDeadBranchSites.isEmpty()) {
120+
// With every site fully enabled, the transformed query is the unreduced one; keep the exact string
121+
// that originally detected the bug rather than re-rendering it (rendering an AST draws random textual
101122
// variants, so a re-render would produce a semantically equal but untested string).
102123
transformedQueryString = initialTransformedQueryString;
103124
return;
104125
}
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.
126+
// Pin the RNG while re-rendering so the same site configuration always yields the same query string; the
127+
// string tested during reduction is then exactly the string the reduced test case reports.
107128
transformedQueryString = Randomly.withFixedSeedRandom(() -> {
108129
// Global site indices are assigned over the fetch columns' records first (in column order), then the
109130
// WHERE clause's record.
110131
List<E> replayedFetchColumns = new ArrayList<>();
111132
int offset = 0;
112133
for (int i = 0; i < fetchColumns.size(); i++) {
113-
int base = offset;
114134
replayedFetchColumns.add(transformer.replay(fetchColumns.get(i), false, fetchColumnRecords.get(i),
115-
site -> enabledSites.contains(base + site)));
135+
directives(enabledSites, constantConditionSites, copiedDeadBranchSites, offset)));
116136
offset += fetchColumnRecords.get(i).getSiteCount();
117137
}
118-
int whereBase = offset;
119138
E replayedWhereClause = transformer.replay(whereClause, true, whereClauseRecord,
120-
site -> enabledSites.contains(whereBase + site));
139+
directives(enabledSites, constantConditionSites, copiedDeadBranchSites, offset));
121140
select.setFetchColumns(replayedFetchColumns);
122141
select.setWhereClause(replayedWhereClause);
123142
return select.asString();
124143
});
125144
}
126145

146+
// Translates the global-index site sets into a record-local directives view starting at the given offset.
147+
private EETTransformer.SiteDirectives directives(Set<Integer> enabledSites, Set<Integer> constantConditionSites,
148+
Set<Integer> copiedDeadBranchSites, int offset) {
149+
return new EETTransformer.SiteDirectives() {
150+
@Override
151+
public boolean isEnabled(int site) {
152+
return enabledSites.contains(offset + site);
153+
}
154+
155+
@Override
156+
public boolean useConstantCondition(int site) {
157+
return constantConditionSites.contains(offset + site);
158+
}
159+
160+
@Override
161+
public boolean useCopiedDeadBranch(int site) {
162+
return copiedDeadBranchSites.contains(offset + site);
163+
}
164+
};
165+
}
166+
127167
@Override
128168
protected List<String> evaluateOriginal(G globalState) throws SQLException {
129169
// Re-execute against the current (reduced) database instead of comparing against a cached result set,

0 commit comments

Comments
 (0)