Skip to content

Commit 570ac85

Browse files
authored
Fixing bug with ARRAY_CONTAINS in Reactive Spring. (Azure#34274)
* Fixing bug with ARRAY_CONTAINS in Reactive Spring. This was already fixed in Spring in the past. * Updating the changelog.
1 parent 9024bcc commit 570ac85

File tree

4 files changed

+110
-14
lines changed

4 files changed

+110
-14
lines changed

sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveTeacherRepositoryIT.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,41 @@
1313
import org.junit.Test;
1414
import org.junit.runner.RunWith;
1515
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.data.domain.Sort;
1617
import org.springframework.test.context.ContextConfiguration;
1718
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
1819
import reactor.core.publisher.Flux;
1920
import reactor.core.publisher.Mono;
2021
import reactor.test.StepVerifier;
2122

23+
import java.util.ArrayList;
2224
import java.util.Arrays;
25+
import java.util.List;
2326

2427
@RunWith(SpringJUnit4ClassRunner.class)
2528
@ContextConfiguration(classes = TestRepositoryConfig.class)
2629
public class ReactiveTeacherRepositoryIT {
2730

2831
private static final String TEACHER_ID_1 = "1";
2932

33+
private static final String TEACHER_ID_2 = "2";
34+
35+
private static final String TEACHER_ID_3 = "3";
36+
3037
private static final String TEACHER_FIRST_NAME_1 = "FirstName1";
3138

39+
private static final String TEACHER_FIRST_NAME_2 = "FirstName2";
40+
3241
private static final String DEPARTMENT_LAST_NAME_1 = "LastName1";
3342

43+
private static final String DEPARTMENT_LAST_NAME_2 = "LastName2";
44+
3445
private static final ReactiveTeacher TEACHER_1 = new ReactiveTeacher(TEACHER_ID_1, TEACHER_FIRST_NAME_1, DEPARTMENT_LAST_NAME_1);
3546

47+
private static final ReactiveTeacher TEACHER_2 = new ReactiveTeacher(TEACHER_ID_2, TEACHER_FIRST_NAME_1, DEPARTMENT_LAST_NAME_2);
48+
49+
private static final ReactiveTeacher TEACHER_3 = new ReactiveTeacher(TEACHER_ID_3, TEACHER_FIRST_NAME_2, DEPARTMENT_LAST_NAME_1);
50+
3651
@ClassRule
3752
public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager();
3853

@@ -67,4 +82,58 @@ public void testSaveWithSuppressedNullValue() {
6782
final Mono<Boolean> existLastNameMono = repository.existsByLastNameIsNull();
6883
StepVerifier.create(existLastNameMono).expectNext(false).expectComplete().verify();
6984
}
85+
86+
@Test
87+
public void testAnnotatedQueryWithArrayContains() {
88+
final Mono<Void> deletedMono = repository.deleteAll();
89+
StepVerifier.create(deletedMono).thenAwait().verifyComplete();
90+
final Flux<ReactiveTeacher> savedFlux = repository.saveAll(Arrays.asList(TEACHER_1, TEACHER_2, TEACHER_3));
91+
StepVerifier.create(savedFlux).thenConsumeWhile(ReactiveTeacher -> true).expectComplete().verify();
92+
93+
List<String> firstNames = new ArrayList<>();
94+
firstNames.add(TEACHER_FIRST_NAME_1);
95+
final Flux<ReactiveTeacher> resultsAsc = repository.annotatedFindByFirstNames(firstNames);
96+
StepVerifier.create(resultsAsc)
97+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1))
98+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2))
99+
.verifyComplete();
100+
101+
List<String> firstNames2 = new ArrayList<>();
102+
firstNames2.add(TEACHER_FIRST_NAME_1);
103+
firstNames2.add(TEACHER_FIRST_NAME_2);
104+
final Flux<ReactiveTeacher> resultsAsc2 = repository.annotatedFindByFirstNames(firstNames2);
105+
StepVerifier.create(resultsAsc2)
106+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1))
107+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2))
108+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_3))
109+
.verifyComplete();
110+
111+
}
112+
113+
@Test
114+
public void testAnnotatedQueryWithArrayContainsAndSort() {
115+
final Mono<Void> deletedMono = repository.deleteAll();
116+
StepVerifier.create(deletedMono).thenAwait().verifyComplete();
117+
final Flux<ReactiveTeacher> savedFlux = repository.saveAll(Arrays.asList(TEACHER_1, TEACHER_2, TEACHER_3));
118+
StepVerifier.create(savedFlux).thenConsumeWhile(ReactiveTeacher -> true).expectComplete().verify();
119+
120+
List<String> firstNames = new ArrayList<>();
121+
firstNames.add(TEACHER_FIRST_NAME_1);
122+
final Flux<ReactiveTeacher> resultsAsc = repository.annotatedFindByFirstNamesWithSort(firstNames, Sort.by(Sort.Direction.DESC, "id"));
123+
StepVerifier.create(resultsAsc)
124+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2))
125+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1))
126+
.verifyComplete();
127+
128+
List<String> firstNames2 = new ArrayList<>();
129+
firstNames2.add(TEACHER_FIRST_NAME_1);
130+
firstNames2.add(TEACHER_FIRST_NAME_2);
131+
final Flux<ReactiveTeacher> resultsAsc2 = repository.annotatedFindByFirstNamesWithSort(firstNames2, Sort.by(Sort.Direction.DESC, "id"));
132+
StepVerifier.create(resultsAsc2)
133+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_3))
134+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2))
135+
.expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1))
136+
.verifyComplete();
137+
138+
}
70139
}

sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveTeacherRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44
package com.azure.spring.data.cosmos.repository.repository;
55

66
import com.azure.spring.data.cosmos.domain.ReactiveTeacher;
7+
import com.azure.spring.data.cosmos.repository.Query;
78
import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository;
9+
import org.springframework.data.domain.Sort;
10+
import org.springframework.data.repository.query.Param;
11+
import reactor.core.publisher.Flux;
812
import reactor.core.publisher.Mono;
913

14+
import java.util.List;
15+
1016

1117
public interface ReactiveTeacherRepository extends ReactiveCosmosRepository<ReactiveTeacher, String> {
1218

1319
Mono<Boolean> existsByFirstNameIsNotNull();
1420

1521
Mono<Boolean> existsByLastNameIsNull();
22+
23+
@Query(value = "SELECT * FROM a WHERE ARRAY_CONTAINS(@firstNames, a.firstName) ")
24+
Flux<ReactiveTeacher> annotatedFindByFirstNames(@Param("firstNames") List<String> firstNames);
25+
26+
@Query(value = "SELECT * FROM a WHERE ARRAY_CONTAINS(@firstNames, a.firstName) ")
27+
Flux<ReactiveTeacher> annotatedFindByFirstNamesWithSort(@Param("firstNames") List<String> firstNames, Sort sort);
1628
}

sdk/cosmos/azure-spring-data-cosmos/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* Added a new flag `overwritePolicy` to `CosmosIndexingPolicy` that when set to true (by default it is false) will allow the user to overwrite the Indexing policy in Portal using the Indexing Policy defined in the SDK. This will affect users who change the Indexing Policy they have defined on the container and want that to overwrite what is in portal, you will now need to set the flag `overwritePolicy` to true for this to happen. The reason we have added this breaking change is that allowing overwrite of an existing indexing policy is considered too risky to be a default behavior. The risk is that you may be removing indexes through multiple indexing policy changes, and in that case the query engine may not provide consistent or complete results until all index transformations are complete. So we are changing the default behavior so that users must opt in to overwriting the indexing policy that exists. - See [PR 33171](https://github.com/Azure/azure-sdk-for-java/pull/33171)
99

1010
#### Bugs Fixed
11-
11+
* Fixing ARRAY_CONTAINS annotated query bug in Reactive Spring introduced by fixing to IN annotated queries. - See [PR 34274](https://github.com/Azure/azure-sdk-for-java/pull/34274)
1212
#### Other Changes
1313

1414
### 3.33.0 (2023-03-17)

sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosParameterParameterAccessor;
1212
import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosQueryMethod;
1313
import com.azure.spring.data.cosmos.repository.query.SimpleReactiveCosmosEntityMetadata;
14+
import org.springframework.data.domain.Pageable;
1415
import org.springframework.data.domain.Sort;
1516
import org.springframework.data.repository.query.Parameter;
1617
import org.springframework.data.repository.query.ResultProcessor;
@@ -20,6 +21,7 @@
2021
import java.util.ArrayList;
2122
import java.util.Collection;
2223
import java.util.List;
24+
import java.util.Locale;
2325
import java.util.stream.Collectors;
2426

2527
import static com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter.toCosmosDbValue;
@@ -53,23 +55,36 @@ public Object execute(final Object[] parameters) {
5355
parameters);
5456
final ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
5557

58+
/*
59+
* The below for loop is used to handle two unique use cases with annotated queries.
60+
* Annotated queries are defined as strings so there is no way to know the clauses
61+
* being used in advance. Some clauses expect an array and others expect just a list of values.
62+
* (1) IN clauses expect the syntax 'IN (a, b, c) which is generated from the if statement.
63+
* (2) ARRAY_CONTAINS expects the syntax 'ARRAY_CONTAINS(["a", "b", "c"], table.param) which
64+
* is generated from the else statement.
65+
*/
5666
String expandedQuery = query;
5767
List<SqlParameter> sqlParameters = new ArrayList<>();
68+
String modifiedExpandedQuery = expandedQuery.toLowerCase(Locale.US).replaceAll("\\s+", "");
5869
for (int paramIndex = 0; paramIndex < parameters.length; paramIndex++) {
5970
Parameter queryParam = getQueryMethod().getParameters().getParameter(paramIndex);
60-
if (parameters[paramIndex] instanceof Collection) {
61-
ArrayList<String> expandParam = (ArrayList<String>) ((Collection<?>) parameters[paramIndex]).stream()
62-
.map(Object::toString).collect(Collectors.toList());
63-
List<String> expandedParamKeys = new ArrayList<>();
64-
for (int arrayIndex = 0; arrayIndex < expandParam.size(); arrayIndex++) {
65-
String paramName = "@" + queryParam.getName().orElse("") + arrayIndex;
66-
expandedParamKeys.add(paramName);
67-
sqlParameters.add(new SqlParameter(paramName, toCosmosDbValue(expandParam.get(arrayIndex))));
68-
}
69-
expandedQuery = expandedQuery.replaceAll("@" + queryParam.getName().orElse(""), String.join(",", expandedParamKeys));
70-
} else {
71-
if (!Sort.class.isAssignableFrom(queryParam.getType())) {
72-
sqlParameters.add(new SqlParameter("@" + queryParam.getName().orElse(""), toCosmosDbValue(parameters[paramIndex])));
71+
String paramName = queryParam.getName().orElse("");
72+
if (!("").equals(paramName)) {
73+
String inParamCheck = "array_contains(@" + paramName.toLowerCase(Locale.US);
74+
if (parameters[paramIndex] instanceof Collection && !modifiedExpandedQuery.contains(inParamCheck)) {
75+
ArrayList<String> expandParam = (ArrayList<String>) ((Collection<?>) parameters[paramIndex]).stream()
76+
.map(Object::toString).collect(Collectors.toList());
77+
List<String> expandedParamKeys = new ArrayList<>();
78+
for (int arrayIndex = 0; arrayIndex < expandParam.size(); arrayIndex++) {
79+
expandedParamKeys.add("@" + paramName + arrayIndex);
80+
sqlParameters.add(new SqlParameter("@" + paramName + arrayIndex, toCosmosDbValue(expandParam.get(arrayIndex))));
81+
}
82+
expandedQuery = expandedQuery.replaceAll("@" + queryParam.getName().orElse(""), String.join(",", expandedParamKeys));
83+
} else {
84+
if (!Pageable.class.isAssignableFrom(queryParam.getType())
85+
&& !Sort.class.isAssignableFrom(queryParam.getType())) {
86+
sqlParameters.add(new SqlParameter("@" + queryParam.getName().orElse(""), toCosmosDbValue(parameters[paramIndex])));
87+
}
7388
}
7489
}
7590
}

0 commit comments

Comments
 (0)