Skip to content

Commit 3dfd0c7

Browse files
committed
Refine StringQuery parameter index allocation.
We now track used parameter indices during String-query rewriting instead of assuming the largest positional parameter index as start index for synthetic and expression parameters. Closes spring-projects#3758
1 parent 88b6ea2 commit 3dfd0c7

File tree

2 files changed

+136
-26
lines changed

2 files changed

+136
-26
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java

+92-26
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.util.ArrayList;
2121
import java.util.Collection;
2222
import java.util.List;
23+
import java.util.Set;
24+
import java.util.TreeSet;
2325
import java.util.function.BiFunction;
2426
import java.util.function.Consumer;
2527
import java.util.function.Function;
@@ -191,6 +193,80 @@ public boolean isNativeQuery() {
191193
return isNative;
192194
}
193195

196+
/**
197+
* Value object to track and allocate used parameter index labels in a query.
198+
*/
199+
static class IndexedParameterLabels {
200+
201+
private final TreeSet<Integer> usedLabels;
202+
private final boolean sequential;
203+
204+
public IndexedParameterLabels(Set<Integer> usedLabels) {
205+
206+
this.usedLabels = usedLabels instanceof TreeSet<Integer> ts ? ts : new TreeSet<Integer>(usedLabels);
207+
this.sequential = isSequential(usedLabels);
208+
}
209+
210+
private static boolean isSequential(Set<Integer> usedLabels) {
211+
212+
for (int i = 0; i < usedLabels.size(); i++) {
213+
214+
if (usedLabels.contains(i + 1)) {
215+
continue;
216+
}
217+
218+
return false;
219+
}
220+
221+
return true;
222+
}
223+
224+
/**
225+
* Allocate the next index label (1-based).
226+
*
227+
* @return the next index label.
228+
*/
229+
public int allocate() {
230+
231+
if (sequential) {
232+
int index = usedLabels.size() + 1;
233+
usedLabels.add(index);
234+
235+
return index;
236+
}
237+
238+
int attempts = usedLabels.last() + 1;
239+
int index = attemptAllocate(attempts);
240+
241+
if (index == -1) {
242+
throw new IllegalStateException(
243+
"Unable to allocate a unique parameter label. All possible labels have been used.");
244+
}
245+
246+
usedLabels.add(index);
247+
248+
return index;
249+
}
250+
251+
private int attemptAllocate(int attempts) {
252+
253+
for (int i = 0; i < attempts; i++) {
254+
255+
if (usedLabels.contains(i + 1)) {
256+
continue;
257+
}
258+
259+
return i + 1;
260+
}
261+
262+
return -1;
263+
}
264+
265+
public boolean hasLabels() {
266+
return !usedLabels.isEmpty();
267+
}
268+
}
269+
194270
/**
195271
* A parser that extracts the parameter bindings from a given query string.
196272
*
@@ -253,28 +329,23 @@ enum ParameterBindingParser {
253329
String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List<ParameterBinding> bindings,
254330
Metadata queryMeta) {
255331

256-
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
257-
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
332+
IndexedParameterLabels parameterLabels = new IndexedParameterLabels(findParameterIndices(query));
333+
boolean parametersShouldBeAccessedByIndex = parameterLabels.hasLabels();
258334

259335
/*
260336
* Prefer indexed access over named parameters if only SpEL Expression parameters are present.
261337
*/
262338
if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) {
263339
parametersShouldBeAccessedByIndex = true;
264-
greatestParameterIndex = 0;
265340
}
266341

267342
ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query,
268-
parametersShouldBeAccessedByIndex, greatestParameterIndex);
343+
parametersShouldBeAccessedByIndex, parameterLabels);
269344

270345
String resultingQuery = parsedQuery.getQueryString();
271346
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
272347

273-
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
274-
int syntheticParameterIndex = expressionParameterIndex + parsedQuery.size();
275-
276-
ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings),
277-
syntheticParameterIndex);
348+
ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings));
278349
int currentIndex = 0;
279350

280351
boolean usesJpaStyleParameters = false;
@@ -309,9 +380,9 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
309380
.getParameter(parameterName == null ? parameterIndexString : parameterName);
310381
String replacement = null;
311382

312-
expressionParameterIndex++;
383+
// this only happens for JDBC-style parameters.
313384
if ("".equals(parameterIndexString)) {
314-
parameterIndex = expressionParameterIndex;
385+
parameterIndex = parameterLabels.allocate();
315386
}
316387

317388
BindingIdentifier queryParameter;
@@ -346,7 +417,7 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
346417
if (origin.isExpression()) {
347418
parameterBindings.register(bindingFactory.apply(queryParameter));
348419
} else {
349-
targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory);
420+
targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory, parameterLabels);
350421
}
351422

352423
replacement = targetBinding.hasName() ? ":" + targetBinding.getName()
@@ -371,16 +442,14 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
371442
}
372443

373444
private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel,
374-
boolean parametersShouldBeAccessedByIndex, int greatestParameterIndex) {
445+
boolean parametersShouldBeAccessedByIndex, IndexedParameterLabels parameterLabels) {
375446

376447
/*
377448
* If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to
378449
* not mix-up with the actual parameter indices.
379450
*/
380-
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
381-
382451
BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
383-
? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1)
452+
? (index, expression) -> String.valueOf(parameterLabels.allocate())
384453
: (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1);
385454

386455
String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
@@ -401,21 +470,21 @@ private static Integer getParameterIndex(@Nullable String parameterIndexString)
401470
return Integer.valueOf(parameterIndexString);
402471
}
403472

404-
private static int tryFindGreatestParameterIndexIn(String query) {
473+
private static Set<Integer> findParameterIndices(String query) {
405474

406475
Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
476+
Set<Integer> usedParameterIndices = new TreeSet<>();
407477

408-
int greatestParameterIndex = -1;
409478
while (parameterIndexMatcher.find()) {
410479

411480
String parameterIndexString = parameterIndexMatcher.group(1);
412481
Integer parameterIndex = getParameterIndex(parameterIndexString);
413482
if (parameterIndex != null) {
414-
greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
483+
usedParameterIndices.add(parameterIndex);
415484
}
416485
}
417486

418-
return greatestParameterIndex;
487+
return usedParameterIndices;
419488
}
420489

421490
private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
@@ -495,17 +564,14 @@ static class ParameterBindings {
495564
private final MultiValueMap<BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
496565

497566
private final Consumer<ParameterBinding> registration;
498-
private int syntheticParameterIndex;
499567

500-
public ParameterBindings(List<ParameterBinding> bindings, Consumer<ParameterBinding> registration,
501-
int syntheticParameterIndex) {
568+
public ParameterBindings(List<ParameterBinding> bindings, Consumer<ParameterBinding> registration) {
502569

503570
for (ParameterBinding binding : bindings) {
504571
this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding)));
505572
}
506573

507574
this.registration = registration;
508-
this.syntheticParameterIndex = syntheticParameterIndex;
509575
}
510576

511577
/**
@@ -519,7 +585,7 @@ public boolean isBound(BindingIdentifier identifier) {
519585
}
520586

521587
BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
522-
Function<BindingIdentifier, ParameterBinding> bindingFactory) {
588+
Function<BindingIdentifier, ParameterBinding> bindingFactory, IndexedParameterLabels parameterLabels) {
523589

524590
Assert.isInstanceOf(MethodInvocationArgument.class, origin);
525591

@@ -554,7 +620,7 @@ BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin,
554620
}
555621
syntheticIdentifier = BindingIdentifier.of(newName);
556622
} else {
557-
syntheticIdentifier = BindingIdentifier.of(++syntheticParameterIndex);
623+
syntheticIdentifier = BindingIdentifier.of(parameterLabels.allocate());
558624
}
559625

560626
ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier);

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java

+44
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,32 @@ void shouldRewritePositionalBindingsWithParameterReuse() {
232232
.containsOnly(1, 2);
233233
}
234234

235+
@Test // GH-3758
236+
void createsDistinctBindingsForIndexedSpel() {
237+
238+
StringQuery query = new StringQuery("select u from User u where u.firstname = ?#{foo} OR u.firstname = ?#{foo}",
239+
false);
240+
241+
assertThat(query.hasParameterBindings()).isTrue();
242+
assertThat(query.getParameterBindings()).hasSize(2).extracting(ParameterBinding::getRequiredPosition)
243+
.containsOnly(1, 2);
244+
assertThat(query.getParameterBindings()).extracting(ParameterBinding::getOrigin)
245+
.extracting(ParameterOrigin::isExpression) //
246+
.containsOnly(true, true);
247+
}
248+
249+
@Test // GH-3758
250+
void createsDistinctBindingsForNamedSpel() {
251+
252+
StringQuery query = new StringQuery("select u from User u where u.firstname = :#{foo} OR u.firstname = :#{foo}",
253+
false);
254+
255+
assertThat(query.hasParameterBindings()).isTrue();
256+
assertThat(query.getParameterBindings()).hasSize(2).extracting(ParameterBinding::getOrigin)
257+
.extracting(ParameterOrigin::isExpression) //
258+
.containsOnly(true, true);
259+
}
260+
235261
@Test // DATAJPA-461
236262
void detectsNamedInParameterBindings() {
237263

@@ -310,6 +336,24 @@ void allowsReuseOfParameterWithInAndRegularBinding() {
310336
assertNamedBinding(InParameterBinding.class, "foo_1", bindings.get(1));
311337
}
312338

339+
@Test // GH-3758
340+
void detectsPositionalInParameterBindingsAndExpressions() {
341+
342+
String queryString = "select u from User u where foo = ?#{bar} and bar = ?3 and baz = ?#{baz}";
343+
StringQuery query = new StringQuery(queryString, true);
344+
345+
assertThat(query.getQueryString()).isEqualTo("select u from User u where foo = ?1 and bar = ?3 and baz = ?2");
346+
}
347+
348+
@Test // GH-3758
349+
void detectsPositionalInParameterBindingsAndExpressionsWithReuse() {
350+
351+
String queryString = "select u from User u where foo = ?#{bar} and bar = ?2 and baz = ?#{bar}";
352+
StringQuery query = new StringQuery(queryString, true);
353+
354+
assertThat(query.getQueryString()).isEqualTo("select u from User u where foo = ?1 and bar = ?2 and baz = ?3");
355+
}
356+
313357
@Test // GH-3126
314358
void countQueryDerivationRetainsNamedExpressionParameters() {
315359

0 commit comments

Comments
 (0)