Skip to content

Commit

Permalink
cosmos spring data - patch support (#32630)
Browse files Browse the repository at this point in the history
* initial patches changes

* initial patch changes

* edits

* add unit tests and Trevor multitenant pieces

* edits after Trevor review

* edits after Trevor review

* remove uneccessary line

* re-factor to fit Spring Data contract better

* fix javadocs errors

* fix javadocs errors

* amend contracts to be more consistent

* amend unit tests

* fix build failures

* Patch improvements with less API and implementation improvements

* fix javadocs format errors

* Adding overrides to revapi.json.

* Revising overrides to revapi.json.

* Revising overrides to revapi.json.

* Fixing reactive tests.

* Updating the changelog.

* address Fabian review + enhance non reactive tests

* revert design + Annie/Trevor review

* fix build errors

* fix unit tests

* update revapi.json

* expand options descriptions in templates

* add respository test cases

* Adding reactive tests and changing one assert.

Co-authored-by: Theo van Kraay <[email protected]>
Co-authored-by: Kushagra Thapar <[email protected]>
Co-authored-by: Trevor Anderson <[email protected]>
  • Loading branch information
4 people authored Jan 12, 2023
1 parent 8bffbf5 commit 0033c98
Show file tree
Hide file tree
Showing 15 changed files with 593 additions and 15 deletions.
48 changes: 48 additions & 0 deletions eng/code-quality-reports/src/main/resources/revapi/revapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,54 @@
"new": "method .* com\\.azure\\.resourcemanager\\..*",
"justification": "resourcemanager interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <T> T com.azure.spring.data.cosmos.core.CosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class<T>, com.azure.cosmos.models.CosmosPatchOperations)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <T> T com.azure.spring.data.cosmos.core.CosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class<T>, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <T> reactor.core.publisher.Mono<T> com.azure.spring.data.cosmos.core.ReactiveCosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class<T>, com.azure.cosmos.models.CosmosPatchOperations)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <T> reactor.core.publisher.Mono<T> com.azure.spring.data.cosmos.core.ReactiveCosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class<T>, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <S extends T> S com.azure.spring.data.cosmos.repository.CosmosRepository<T, ID extends java.io.Serializable>::save(ID, com.azure.cosmos.models.PartitionKey, java.lang.Class<S>, com.azure.cosmos.models.CosmosPatchOperations)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <S extends T> S com.azure.spring.data.cosmos.repository.CosmosRepository<T, ID extends java.io.Serializable>::save(ID, com.azure.cosmos.models.PartitionKey, java.lang.Class<S>, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <S extends T> reactor.core.publisher.Mono<S> com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository<T, K>::save(K, com.azure.cosmos.models.PartitionKey, java.lang.Class<S>, com.azure.cosmos.models.CosmosPatchOperations)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"ignore": true,
"code": "java.method.addedToInterface",
"new": "method <S extends T> reactor.core.publisher.Mono<S> com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository<T, K>::save(K, com.azure.cosmos.models.PartitionKey, java.lang.Class<S>, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)",
"justification": "Spring interfaces are allowed to add methods."
},
{
"regex": true,
"code": "java\\.class\\.externalClassExposedInAPI",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import com.azure.cosmos.models.IndexingMode;
import com.azure.spring.data.cosmos.domain.Address;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.HashMap;
Expand All @@ -18,7 +20,9 @@ public final class TestConstants {
private static final Address ADDRESS_1 = new Address("201107", "Zixing Road", "Shanghai");
private static final Address ADDRESS_2 = new Address("200000", "Xuhui", "Shanghai");
public static final String HOBBY1 = "photography";
public static final String PATCH_HOBBY1 = "shopping";
public static final List<String> HOBBIES = Arrays.asList(HOBBY1, "fishing");
public static final List<String> PATCH_HOBBIES = Arrays.asList(HOBBY1, "fishing", PATCH_HOBBY1);
public static final List<Address> ADDRESSES = Arrays.asList(ADDRESS_1, ADDRESS_2);

public static final String ROLE_COLLECTION_NAME = "RoleCollectionName";
Expand All @@ -45,6 +49,7 @@ public final class TestConstants {

public static final String DB_NAME = "testdb";
public static final String FIRST_NAME = "first_name_li";
public static final String PATCH_FIRST_NAME = "first_name_replace";
public static final String LAST_NAME = "last_name_p";
public static final Integer ZIP_CODE = 12345;
public static final String ID_1 = "id-1";
Expand Down Expand Up @@ -98,12 +103,19 @@ public final class TestConstants {
public static final String DEPARTMENT = "test-department";

public static final Integer AGE = 24;
public static final Integer PATCH_AGE_1 = 25;
public static final Integer PATCH_AGE_INCREMENT = 2;

public static final Map<String, String> PASSPORT_IDS_BY_COUNTRY = new HashMap<String, String>() {{
put("United States of America", "123456789");
put("Côte d'Ivoire", "IC1234567");
}};

public static final Map<String, String> NEW_PASSPORT_IDS_BY_COUNTRY = new HashMap<String, String>() {{
put("United Kingdom", "123456789");
put("Germany", "IC1234567");
}};

private TestConstants() {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import com.azure.cosmos.CosmosClientBuilder;
import com.azure.cosmos.CosmosException;
import com.azure.cosmos.implementation.ConflictException;
import com.azure.cosmos.implementation.PreconditionFailedException;
import com.azure.cosmos.models.CosmosContainerProperties;
import com.azure.cosmos.models.CosmosPatchItemRequestOptions;
import com.azure.cosmos.models.CosmosPatchOperations;
import com.azure.cosmos.models.PartitionKey;
import com.azure.cosmos.models.SqlQuerySpec;
import com.azure.cosmos.models.ThroughputResponse;
Expand Down Expand Up @@ -35,6 +38,9 @@
import com.azure.spring.data.cosmos.repository.TestRepositoryConfig;
import com.azure.spring.data.cosmos.repository.repository.AuditableRepository;
import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.util.Lists;
import org.junit.Before;
import org.junit.ClassRule;
Expand Down Expand Up @@ -71,11 +77,17 @@
import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME;
import static com.azure.spring.data.cosmos.common.TestConstants.NEW_FIRST_NAME;
import static com.azure.spring.data.cosmos.common.TestConstants.NEW_LAST_NAME;
import static com.azure.spring.data.cosmos.common.TestConstants.NEW_PASSPORT_IDS_BY_COUNTRY;
import static com.azure.spring.data.cosmos.common.TestConstants.NOT_EXIST_ID;
import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_1;
import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_2;
import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_3;
import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_1;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_INCREMENT;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_FIRST_NAME;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBIES;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBY1;
import static com.azure.spring.data.cosmos.common.TestConstants.UPDATED_FIRST_NAME;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
Expand All @@ -98,6 +110,24 @@ public class CosmosTemplateIT {

private static final String WRONG_ETAG = "WRONG_ETAG";

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final JsonNode NEW_PASSPORT_IDS_BY_COUNTRY_JSON = OBJECT_MAPPER.convertValue(NEW_PASSPORT_IDS_BY_COUNTRY, JsonNode.class);

private static final CosmosPatchOperations operations = CosmosPatchOperations
.create()
.replace("/age", PATCH_AGE_1);

CosmosPatchOperations multiPatchOperations = CosmosPatchOperations
.create()
.set("/firstName", PATCH_FIRST_NAME)
.replace("/passportIdsByCountry", NEW_PASSPORT_IDS_BY_COUNTRY_JSON)
.add("/hobbies/2", PATCH_HOBBY1)
.remove("/shippingAddresses/1")
.increment("/age", PATCH_AGE_INCREMENT);

private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions();


@ClassRule
public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager();

Expand All @@ -117,6 +147,9 @@ public class CosmosTemplateIT {
@Autowired
private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils;

public CosmosTemplateIT() throws JsonProcessingException {
}

@Before
public void setUp() throws ClassNotFoundException {
if (cosmosTemplate == null) {
Expand Down Expand Up @@ -267,6 +300,42 @@ public void testUpdate() {
assertEquals(person, updated);
}

@Test
public void testPatch() {
Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations);
assertEquals(patchedPerson.getAge(), PATCH_AGE_1);
}

@Test
public void testPatchMultiOperations() {
Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, multiPatchOperations);
assertEquals(patchedPerson.getAge().intValue(), (AGE + PATCH_AGE_INCREMENT));
assertEquals(patchedPerson.getHobbies(), PATCH_HOBBIES);
assertEquals(patchedPerson.getFirstName(), PATCH_FIRST_NAME);
assertEquals(patchedPerson.getShippingAddresses().size(), 1);
assertEquals(patchedPerson.getPassportIdsByCountry(), NEW_PASSPORT_IDS_BY_COUNTRY);
}

@Test
public void testPatchPreConditionSuccess() {
options.setFilterPredicate("FROM person p WHERE p.lastName = '"+LAST_NAME+"'");
Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options);
assertEquals(patchedPerson.getAge(), PATCH_AGE_1);
}

@Test
public void testPatchPreConditionFail() {
try {
options.setFilterPredicate("FROM person p WHERE p.lastName = 'dummy'");
Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options);
assertEquals(patchedPerson.getAge(), PATCH_AGE_1);
fail();
} catch (CosmosAccessException ex) {
assertThat(ex.getCosmosException()).isInstanceOf(PreconditionFailedException.class);
assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull();
}
}

@Test
public void testOptimisticLockWhenUpdatingWithWrongEtag() {
final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import com.azure.cosmos.CosmosClientBuilder;
import com.azure.cosmos.CosmosException;
import com.azure.cosmos.implementation.ConflictException;
import com.azure.cosmos.implementation.PreconditionFailedException;
import com.azure.cosmos.models.CosmosContainerResponse;
import com.azure.cosmos.models.CosmosPatchItemRequestOptions;
import com.azure.cosmos.models.CosmosPatchOperations;
import com.azure.cosmos.models.PartitionKey;
import com.azure.cosmos.models.SqlQuerySpec;
import com.azure.cosmos.models.ThroughputResponse;
Expand All @@ -33,6 +36,8 @@
import com.azure.spring.data.cosmos.repository.TestRepositoryConfig;
import com.azure.spring.data.cosmos.repository.repository.AuditableRepository;
import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Assert;
Expand Down Expand Up @@ -65,7 +70,13 @@
import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME;
import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES;
import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME;
import static com.azure.spring.data.cosmos.common.TestConstants.NEW_PASSPORT_IDS_BY_COUNTRY;
import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_1;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_INCREMENT;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_FIRST_NAME;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBIES;
import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBY1;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
Expand All @@ -89,6 +100,23 @@ public class ReactiveCosmosTemplateIT {
private static final String PRECONDITION_IS_NOT_MET = "is not met";
private static final String WRONG_ETAG = "WRONG_ETAG";

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final JsonNode NEW_PASSPORT_IDS_BY_COUNTRY_JSON = OBJECT_MAPPER.convertValue(NEW_PASSPORT_IDS_BY_COUNTRY, JsonNode.class);

private static final CosmosPatchOperations operations = CosmosPatchOperations
.create()
.replace("/age", PATCH_AGE_1);

CosmosPatchOperations multiPatchOperations = CosmosPatchOperations
.create()
.set("/firstName", PATCH_FIRST_NAME)
.replace("/passportIdsByCountry", NEW_PASSPORT_IDS_BY_COUNTRY_JSON)
.add("/hobbies/2", PATCH_HOBBY1)
.remove("/shippingAddresses/1")
.increment("/age", PATCH_AGE_INCREMENT);

private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions();

@ClassRule
public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager();

Expand Down Expand Up @@ -281,6 +309,41 @@ public void testUpsert() {
assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull();
}

@Test
public void testPatch() {
final Mono<Person> patch = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations);
StepVerifier.create(patch).expectNextCount(1).verifyComplete();
Mono<Person> patchedPerson = cosmosTemplate.findById(containerName, insertedPerson.getId(), Person.class);
StepVerifier.create(patchedPerson).expectNextMatches(person -> person.getAge() == PATCH_AGE_1).verifyComplete();
}

@Test
public void testPatchMultiOperations() {
final Mono<Person> patch = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, multiPatchOperations);
StepVerifier.create(patch).expectNextCount(1).verifyComplete();
Person patchedPerson = cosmosTemplate.findById(containerName, insertedPerson.getId(), Person.class).block();
assertEquals(patchedPerson.getAge().intValue(), (AGE + PATCH_AGE_INCREMENT));
assertEquals(patchedPerson.getHobbies(),PATCH_HOBBIES);
assertEquals(patchedPerson.getFirstName(), PATCH_FIRST_NAME);
assertEquals(patchedPerson.getShippingAddresses().size(), 1);
assertEquals(patchedPerson.getPassportIdsByCountry(), NEW_PASSPORT_IDS_BY_COUNTRY);
}

@Test
public void testPatchPreConditionSuccess() {
options.setFilterPredicate("FROM person p WHERE p.lastName = '"+LAST_NAME+"'");
Mono<Person> patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options);
StepVerifier.create(patchedPerson).expectNextMatches(person -> person.getAge() == PATCH_AGE_1).verifyComplete();
}

@Test
public void testPatchPreConditionFail() {
options.setFilterPredicate("FROM person p WHERE p.lastName = 'dummy'");
Mono<Person> person = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options);
StepVerifier.create(person).expectErrorMatches(ex -> ex instanceof CosmosAccessException &&
((CosmosAccessException) ex).getCosmosException() instanceof PreconditionFailedException).verify();
}

@Test
public void testOptimisticLockWhenUpdatingWithWrongEtag() {
final Person updated = new Person(TEST_PERSON.getId(), TestConstants.UPDATED_FIRST_NAME,
Expand Down
Loading

0 comments on commit 0033c98

Please sign in to comment.