Skip to content

Commit e5f9039

Browse files
fix: Sort spel parameters by the length of their name before replacing them with literals.
Fixes #2947.
1 parent 40466d0 commit e5f9039

File tree

4 files changed

+63
-26
lines changed

4 files changed

+63
-26
lines changed

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java

+21-22
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
import java.util.Arrays;
2121
import java.util.Collection;
2222
import java.util.Collections;
23+
import java.util.Comparator;
2324
import java.util.HashMap;
2425
import java.util.HashSet;
25-
import java.util.Iterator;
2626
import java.util.LinkedHashMap;
2727
import java.util.List;
2828
import java.util.Map;
@@ -245,37 +245,36 @@ static class QueryContext {
245245

246246
final Map<String, Object> boundParameters;
247247

248-
String query;
248+
final String query;
249249

250250
private boolean hasLiteralReplacementForSort = false;
251251

252252
QueryContext(String repositoryMethodName, String template, Map<String, Object> boundParameters) {
253253
this.repositoryMethodName = repositoryMethodName;
254254
this.template = template;
255-
this.query = this.template;
256255
this.boundParameters = boundParameters;
257-
}
258-
}
259256

260-
void replaceLiteralsIn(QueryContext queryContext) {
261-
262-
String cypherQuery = queryContext.template;
263-
Iterator<Map.Entry<String, Object>> iterator = queryContext.boundParameters.entrySet().iterator();
264-
while (iterator.hasNext()) {
265-
Map.Entry<String, Object> entry = iterator.next();
266-
Object value = entry.getValue();
267-
if (!(value instanceof Neo4jSpelSupport.LiteralReplacement)) {
268-
continue;
257+
String cypherQuery = this.template;
258+
Comparator<Map.Entry<String, Object>> byLengthDescending = Comparator.comparing(e -> e.getKey().length());
259+
byLengthDescending = byLengthDescending.reversed();
260+
List<Map.Entry<String, Object>> entries = this.boundParameters.entrySet()
261+
.stream().sorted(byLengthDescending)
262+
.toList();
263+
for (var entry : entries) {
264+
Object value = entry.getValue();
265+
if (!(value instanceof Neo4jSpelSupport.LiteralReplacement)) {
266+
continue;
267+
}
268+
this.boundParameters.remove(entry.getKey());
269+
270+
String key = entry.getKey();
271+
cypherQuery = cypherQuery.replace("$" + key, ((Neo4jSpelSupport.LiteralReplacement) value).getValue());
272+
this.hasLiteralReplacementForSort =
273+
this.hasLiteralReplacementForSort ||
274+
((Neo4jSpelSupport.LiteralReplacement) value).getTarget() == Neo4jSpelSupport.LiteralReplacement.Target.SORT;
269275
}
270-
iterator.remove();
271-
272-
String key = entry.getKey();
273-
cypherQuery = cypherQuery.replace("$" + key, ((Neo4jSpelSupport.LiteralReplacement) value).getValue());
274-
queryContext.hasLiteralReplacementForSort =
275-
queryContext.hasLiteralReplacementForSort ||
276-
((Neo4jSpelSupport.LiteralReplacement) value).getTarget() == Neo4jSpelSupport.LiteralReplacement.Target.SORT;
276+
this.query = cypherQuery;
277277
}
278-
queryContext.query = cypherQuery;
279278
}
280279

281280
void logWarningsIfNecessary(QueryContext queryContext, Neo4jParameterAccessor parameterAccessor) {

src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java

-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ protected <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType
138138
boundParameters
139139
);
140140

141-
replaceLiteralsIn(queryContext);
142141
logWarningsIfNecessary(queryContext, parameterAccessor);
143142

144143
return PreparedQuery.queryFor(returnedType)

src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java

-3
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ protected <T extends Object> PreparedQuery<T> prepareQuery(Class<T> returnedType
188188
boundParameters
189189
);
190190

191-
replaceLiteralsIn(queryContext);
192191
logWarningsIfNecessary(queryContext, parameterAccessor);
193192

194193
return PreparedQuery.queryFor(returnedType)
@@ -247,8 +246,6 @@ protected Optional<PreparedQuery<Long>> getCountQuery(Neo4jParameterAccessor par
247246
boundParameters
248247
);
249248

250-
replaceLiteralsIn(queryContext);
251-
252249
return PreparedQuery.queryFor(Long.class)
253250
.withCypherQuery(queryContext.query)
254251
.withParameters(boundParameters)

src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java

+42
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.ArrayList;
2424
import java.util.Collection;
2525
import java.util.Collections;
26+
import java.util.HashMap;
2627
import java.util.IdentityHashMap;
2728
import java.util.Map;
2829
import java.util.concurrent.Callable;
@@ -43,6 +44,7 @@
4344
import org.springframework.data.neo4j.core.schema.Node;
4445
import org.springframework.data.neo4j.repository.query.Neo4jSpelSupport.LiteralReplacement;
4546
import org.springframework.data.repository.core.EntityMetadata;
47+
import org.springframework.data.repository.query.SpelQueryContext;
4648
import org.springframework.expression.spel.standard.SpelExpressionParser;
4749
import org.springframework.util.ReflectionUtils;
4850

@@ -183,6 +185,46 @@ void shouldUnquoteParameterExpressionsCorrectly(String quoted, String expected)
183185
assertThat(query).isEqualTo(expected);
184186
}
185187

188+
@Test
189+
void moreThan10SpelEntriesShouldWork() {
190+
191+
SpelQueryContext spelQueryContext = StringBasedNeo4jQuery.SPEL_QUERY_CONTEXT;
192+
193+
StringBuilder template = new StringBuilder("MATCH (user:User) WHERE ");
194+
String query;
195+
SpelQueryContext.SpelExtractor spelExtractor;
196+
197+
class R implements LiteralReplacement {
198+
private final String value;
199+
200+
R(String value) {
201+
this.value = value;
202+
}
203+
204+
@Override
205+
public String getValue() {
206+
return value;
207+
}
208+
209+
@Override
210+
public Target getTarget() {
211+
return Target.UNSPECIFIED;
212+
}
213+
}
214+
215+
Map<String, Object> parameters = new HashMap<>();
216+
for (int i = 0; i <= 20; ++i) {
217+
template.append("user.name = :#{#searchUser.name} OR ");
218+
parameters.put("__SpEL__" + i, new R("'x" + i + "'"));
219+
}
220+
template.delete(template.length() - 4, template.length());
221+
spelExtractor = spelQueryContext.parse(template.toString());
222+
query = spelExtractor.getQueryString();
223+
Neo4jQuerySupport.QueryContext qc = new Neo4jQuerySupport.QueryContext("n/a", query, parameters);
224+
assertThat(qc.query).isEqualTo(
225+
"MATCH (user:User) WHERE user.name = 'x0' OR user.name = 'x1' OR user.name = 'x2' OR user.name = 'x3' OR user.name = 'x4' OR user.name = 'x5' OR user.name = 'x6' OR user.name = 'x7' OR user.name = 'x8' OR user.name = 'x9' OR user.name = 'x10' OR user.name = 'x11' OR user.name = 'x12' OR user.name = 'x13' OR user.name = 'x14' OR user.name = 'x15' OR user.name = 'x16' OR user.name = 'x17' OR user.name = 'x18' OR user.name = 'x19' OR user.name = 'x20'");
226+
}
227+
186228
@Test // GH-2279
187229
void shouldQuoteParameterExpressionsCorrectly() {
188230

0 commit comments

Comments
 (0)