55import java .util .ArrayList ;
66import java .util .HashSet ;
77import java .util .List ;
8+ import java .util .Set ;
89
910import 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
0 commit comments