Skip to content

Commit 09d7e13

Browse files
plalxvelo
andauthored
Fix nondeterministic constant ordering causing possible incorrect in-memory evaluation (#1803)
* Fix nondeterministic constant ordering causing incorrect in-memory evaluation Problem: `DefaultEvaluatorFactory` builds compiled evaluators with from a non-deterministic ordering of constants. This can lead to mismatches between the cached generated java method and the runtime argument order, producing incorrect evaluation results. `AbstractEvaluatorFactory#toId` only includes constant types in the cache key, not their values. This is safe only if argument ordering is stable, which is not guaranteed with the current constant resolution logic. For example, the predicate `QEntity.entity.prop1.eq(true).or(QEntity.entity.prop2.eq(false))` may result in a mismatch where the same compiled method is reused, but arguments are supplied in a different order. The method may be invoked with `[true, false]` (correct) or `[false, true]` (incorrect), leading to inverted semantics at evaluation time. Solution: This change ensures deterministic constant ordering so that compiled methods and runtime arguments always remain aligned. * Apply code formatting to DefaultEvaluatorFactory Signed-off-by: Marvin Froeder <velo.br@gmail.com> * Add regression test for deterministic constant ordering Signed-off-by: Marvin Froeder <velo.br@gmail.com> --------- Signed-off-by: Marvin Froeder <velo.br@gmail.com> Co-authored-by: Alexandre Potvin Latreille <alexandre.potvinlatreille@cra-arc.gc.ca> Co-authored-by: Marvin Froeder <velo.br@gmail.com>
1 parent 5584909 commit 09d7e13

2 files changed

Lines changed: 93 additions & 17 deletions

File tree

querydsl-libraries/querydsl-collections/src/main/java/com/querydsl/collections/DefaultEvaluatorFactory.java

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import com.querydsl.core.util.PrimitiveUtils;
3838
import java.net.URLClassLoader;
3939
import java.util.ArrayList;
40-
import java.util.HashMap;
40+
import java.util.LinkedHashMap;
4141
import java.util.List;
4242
import java.util.Map;
4343
import javax.tools.JavaCompiler;
@@ -112,8 +112,8 @@ public <T> Evaluator<T> create(
112112
}
113113
serializer.append(";");
114114

115-
var constantToLabel = serializer.getConstantToLabel();
116-
var constants = getConstants(metadata, constantToLabel);
115+
var constants =
116+
getConstants(metadata, serializer.getConstants(), serializer.getConstantToLabel());
117117
var types = new Class<?>[sources.size()];
118118
var names = new String[sources.size()];
119119
for (var i = 0; i < sources.size(); i++) {
@@ -155,8 +155,7 @@ public <T> Evaluator<List<T>> createEvaluator(
155155
ser.append("}\n");
156156
ser.append("return rv;");
157157

158-
var constantToLabel = ser.getConstantToLabel();
159-
var constants = getConstants(metadata, constantToLabel);
158+
var constants = getConstants(metadata, ser.getConstants(), ser.getConstantToLabel());
160159

161160
Type sourceType = new ClassType(TypeCategory.SIMPLE, source.getType());
162161
var sourceListType = new ClassType(TypeCategory.SIMPLE, Iterable.class, sourceType);
@@ -269,8 +268,7 @@ public Evaluator<List<Object[]>> createEvaluator(
269268
}
270269
ser.append("return rv;");
271270

272-
var constantToLabel = ser.getConstantToLabel();
273-
var constants = getConstants(metadata, constantToLabel);
271+
var constants = getConstants(metadata, ser.getConstants(), ser.getConstantToLabel());
274272

275273
var projectionType = new ClassType(TypeCategory.LIST, List.class, Types.OBJECTS);
276274
return factory.createEvaluator(
@@ -283,19 +281,25 @@ public Evaluator<List<Object[]>> createEvaluator(
283281
}
284282

285283
private Map<String, Object> getConstants(
286-
QueryMetadata metadata, Map<Object, String> constantToLabel) {
287-
Map<String, Object> constants = new HashMap<>();
288-
for (Map.Entry<Object, String> entry : constantToLabel.entrySet()) {
289-
if (entry.getKey() instanceof ParamExpression<?>) {
290-
var value = metadata.getParams().get(entry.getKey());
284+
QueryMetadata metadata, List<Object> constants, Map<Object, String> constantToLabel) {
285+
var result = new LinkedHashMap<String, Object>();
286+
287+
for (var constant : constants) {
288+
var paramName = constantToLabel.get(constant);
289+
290+
if (constant instanceof ParamExpression<?>) {
291+
var value = metadata.getParams().get(constant);
291292
if (value == null) {
292-
throw new ParamNotSetException((ParamExpression<?>) entry.getKey());
293+
throw new ParamNotSetException((ParamExpression<?>) constant);
293294
}
294-
constants.put(entry.getValue(), value);
295-
} else {
296-
constants.put(entry.getValue(), entry.getKey());
295+
296+
result.put(paramName, value);
297+
continue;
297298
}
299+
300+
result.put(paramName, constant);
298301
}
299-
return constants;
302+
303+
return result;
300304
}
301305
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.querydsl.collections;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.querydsl.codegen.utils.Evaluator;
6+
import com.querydsl.codegen.utils.EvaluatorFactory;
7+
import com.querydsl.codegen.utils.model.ClassType;
8+
import com.querydsl.codegen.utils.model.Type;
9+
import com.querydsl.core.DefaultQueryMetadata;
10+
import com.querydsl.core.types.dsl.BooleanExpression;
11+
import java.util.ArrayList;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.Test;
14+
15+
public class DefaultEvaluatorFactoryTest extends AbstractQueryTest {
16+
17+
/**
18+
* The compiled evaluator is cached by a key that does not encode constant ordering, and its
19+
* arguments are supplied positionally. The factory must therefore emit constants in a
20+
* deterministic order matching the AST traversal so that a cached method is always invoked with
21+
* its arguments in the same positions it was compiled with.
22+
*/
23+
@Test
24+
public void constants_follow_ast_order() {
25+
var capturing = new CapturingEvaluatorFactory();
26+
var factory = new DefaultEvaluatorFactory(CollQueryTemplates.DEFAULT, capturing);
27+
28+
BooleanExpression filter = cat.name.eq("c1");
29+
for (var i = 2; i <= 12; i++) {
30+
filter = filter.or(cat.name.eq("c" + i));
31+
}
32+
33+
factory.createEvaluator(new DefaultQueryMetadata(), cat, filter);
34+
35+
var expectedKeys = new ArrayList<String>();
36+
for (var i = 1; i <= 12; i++) {
37+
expectedKeys.add("a" + i);
38+
}
39+
assertThat(capturing.constants.keySet()).containsExactlyElementsOf(expectedKeys);
40+
for (var i = 1; i <= 12; i++) {
41+
assertThat(capturing.constants.get("a" + i)).isEqualTo("c" + i);
42+
}
43+
}
44+
45+
private static final class CapturingEvaluatorFactory implements EvaluatorFactory {
46+
47+
private Map<String, Object> constants;
48+
49+
@Override
50+
public <T> Evaluator<T> createEvaluator(
51+
String source,
52+
Class<? extends T> projectionType,
53+
String[] names,
54+
Class<?>[] classes,
55+
Map<String, Object> constants) {
56+
this.constants = constants;
57+
return null;
58+
}
59+
60+
@Override
61+
public <T> Evaluator<T> createEvaluator(
62+
String source,
63+
ClassType projection,
64+
String[] names,
65+
Type[] types,
66+
Class<?>[] classes,
67+
Map<String, Object> constants) {
68+
this.constants = constants;
69+
return null;
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)