diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-a423163.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-a423163.json new file mode 100644 index 000000000000..0110759e2cb0 --- /dev/null +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-a423163.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Increase functional tests coverage on dynamodb-enhanced module" +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java index 14628e4aa3b9..11f982ae5b7b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java @@ -318,7 +318,14 @@ private void processFlattenedNestedAttributes( } for (String attrName : customMetadataObject) { - AttributeConverter converter = schema.converterForAttribute(attrName); + AttributeConverter converter; + try { + converter = schema.converterForAttribute(attrName); + } catch (UnsupportedOperationException e) { + // Some custom/third-party TableSchema implementations don't support converterForAttribute. + // In that case, skip timestamp insertion for this attribute instead of failing the write. + continue; + } if (converter != null) { insertTimestampInItemToTransform(updatedItems, reconstructCompositeKey(path, attrName), @@ -390,7 +397,13 @@ private Map applyAutoGeneratedTimestampsToMap( boolean mapCopied = false; for (String key : customMetadataObject) { - AttributeConverter converter = nestedSchema.converterForAttribute(key); + AttributeConverter converter; + try { + converter = nestedSchema.converterForAttribute(key); + } catch (UnsupportedOperationException e) { + // Nested schema can't resolve converters: skip timestamp insertion for this attribute. + continue; + } if (converter != null) { if (!mapCopied) { updatedNestedMap = new HashMap<>(nestedMap); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAtomicCounterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAtomicCounterTest.java new file mode 100644 index 000000000000..1928f72ecd9d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAtomicCounterTest.java @@ -0,0 +1,46 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AtomicCounterRecord; + +public class AsyncAtomicCounterTest extends LocalDynamoDbAsyncTestBase { + private static final TableSchema TABLE_SCHEMA = TableSchema.fromClass(AtomicCounterRecord.class); + + private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = + DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build(); + + private final DynamoDbAsyncTable mappedTable = + enhancedAsyncClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))).join(); + } + + @Test + public void repeatedUpdate_shouldIncrementCountersOnEachUpdate() { + AtomicCounterRecord record = new AtomicCounterRecord(); + record.setId("id1"); + record.setAttribute1("value"); + mappedTable.updateItem(record).join(); + mappedTable.updateItem(record).join(); + mappedTable.updateItem(record).join(); + + AtomicCounterRecord persisted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); + assertThat(persisted.getDefaultCounter()).isEqualTo(2L); + assertThat(persisted.getCustomCounter()).isEqualTo(20L); + assertThat(persisted.getDecreasingCounter()).isEqualTo(-22L); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAutoGeneratedUuidRecordTest.java new file mode 100644 index 000000000000..22fc9f763fef --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAutoGeneratedUuidRecordTest.java @@ -0,0 +1,86 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; + +import java.util.UUID; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; + +public class AsyncAutoGeneratedUuidRecordTest extends LocalDynamoDbAsyncTestBase { + private static class Record { + private String id; + private String writeAlwaysUuid; + private String writeIfNotExistsUuid; + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getWriteAlwaysUuid() { return writeAlwaysUuid; } + public void setWriteAlwaysUuid(String writeAlwaysUuid) { this.writeAlwaysUuid = writeAlwaysUuid; } + public String getWriteIfNotExistsUuid() { return writeIfNotExistsUuid; } + public void setWriteIfNotExistsUuid(String writeIfNotExistsUuid) { this.writeIfNotExistsUuid = writeIfNotExistsUuid; } + } + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, a -> a.name("id").getter(Record::getId).setter(Record::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("writeAlwaysUuid") + .getter(Record::getWriteAlwaysUuid) + .setter(Record::setWriteAlwaysUuid) + .tags(autoGeneratedUuidAttribute())) + .addAttribute(String.class, a -> a.name("writeIfNotExistsUuid") + .getter(Record::getWriteIfNotExistsUuid) + .setter(Record::setWriteIfNotExistsUuid) + .tags(autoGeneratedUuidAttribute(), + updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .build(); + + private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = + DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedUuidExtension.create()) + .build(); + + private final DynamoDbAsyncTable mappedTable = enhancedAsyncClient.table(getConcreteTableName("table-name"), + TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))).join(); + } + + @Test + public void putOverwrite_withExplicitUuidValues_shouldRegenerateWriteAlwaysAndNotPreserveWriteIfNotExists() { + Record seed = new Record(); + seed.setId("id1"); + mappedTable.putItem(seed).join(); + + Record overwrite = new Record(); + overwrite.setId("id1"); + overwrite.setWriteAlwaysUuid("manual-a"); + overwrite.setWriteIfNotExistsUuid("manual-b"); + mappedTable.putItem(overwrite).join(); + Record persisted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); + + assertThat(persisted.getWriteAlwaysUuid()).isNotEqualTo("manual-a"); + UUID.fromString(persisted.getWriteAlwaysUuid()); + assertThat(persisted.getWriteIfNotExistsUuid()).isNotEqualTo("manual-b"); + assertThat(persisted.getWriteIfNotExistsUuid()).isNotNull(); + UUID.fromString(persisted.getWriteIfNotExistsUuid()); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicCrudTest.java index 7901f42fe594..f3637f439427 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicCrudTest.java @@ -18,6 +18,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; @@ -25,7 +26,10 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; +import java.util.Collections; import java.util.Objects; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletionException; import org.junit.After; import org.junit.Before; @@ -37,6 +41,8 @@ import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; @@ -44,6 +50,7 @@ import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ProjectionType; @@ -677,4 +684,29 @@ public void getAShortRecordWithNewModelledFields() { Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); assertThat(result, is(expectedRecord)); } + + @Test + public void extensionEnabledClient_withUnsupportedCustomSchema_putAndUpdateDoNotThrow() { + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable> customTable = + extensionClient.table(getConcreteTableName("crud-custom-table"), new UnsupportedConverterTableSchema()); + customTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + Map item = new HashMap<>(); + item.put("pk", AttributeValue.builder().s("custom-1").build()); + item.put("nested", + AttributeValue.builder() + .m(Collections.singletonMap("k", AttributeValue.builder().s("v").build())) + .build()); + + customTable.putItem(item).join(); + customTable.updateItem(item).join(); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))).join(); + assertThat(persisted, is(notNullValue())); + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("crud-custom-table"))).join(); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicQueryTest.java index b7fe4c041801..7c7108d8d15b 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicQueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicQueryTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @@ -25,6 +26,8 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -40,7 +43,10 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.Page; @@ -48,6 +54,7 @@ import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.Select; public class AsyncBasicQueryTest extends LocalDynamoDbAsyncTestBase { private static class Record { @@ -119,19 +126,42 @@ public int hashCode() { .mapToObj(i -> new Record().setId("id-value").setSort(i).setValue(i)) .collect(Collectors.toList()); + private static final List NESTED_TEST_RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> { + NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-" + i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v" + i); + return nestedTestRecord; + }) + .collect(Collectors.toList()); + private DynamoDbEnhancedAsyncClient enhancedAsyncClient = DefaultDynamoDbEnhancedAsyncClient.builder() .dynamoDbClient(getDynamoDbAsyncClient()) .build(); private DynamoDbAsyncTable mappedTable = enhancedAsyncClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private DynamoDbAsyncTable mappedNestedTable = + enhancedAsyncClient.table(getConcreteTableName("nested-table-name"), TableSchema.fromClass(NestedTestRecord.class)); private void insertRecords() { RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record)).join()); + NESTED_TEST_RECORDS.forEach(record -> mappedNestedTable.putItem(r -> r.item(record)).join()); + } + + private void insertNestedRecords() { + NESTED_TEST_RECORDS.forEach(record -> mappedNestedTable.putItem(r -> r.item(record)).join()); } @Before public void createTable() { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + mappedNestedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); } @After @@ -140,6 +170,10 @@ public void deleteTable() { .tableName(getConcreteTableName("table-name")) .build()) .join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("nested-table-name")) + .build()) + .join(); } @Test @@ -182,6 +216,22 @@ public void queryAllRecordsWithFilter() { assertThat(page.lastEvaluatedKey(), is(nullValue())); } + @Test + public void queryAllRecords_withAttributeProjection_shouldExcludeUnprojectedAttributes() { + insertRecords(); + SdkPublisher> publisher = + mappedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .attributesToProject("value")); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + assertThat(page.items().size(), is(RECORDS.size())); + Record firstRecord = page.items().get(0); + assertThat(firstRecord.id, is(nullValue())); + assertThat(firstRecord.sort, is(nullValue())); + assertThat(firstRecord.value, is(0)); + } + @Test public void queryAllRecordsWithFilter_viaItems() { insertRecords(); @@ -314,4 +364,72 @@ public void queryExclusiveStartKey() { assertThat(page.items(), is(RECORDS.subList(8, 10))); assertThat(page.lastEvaluatedKey(), is(nullValue())); } + + @Test + public void queryAllRecords_withSelectCount_shouldReturnCountNotItems() { + insertRecords(); + SdkPublisher> publisher = + mappedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .select(Select.COUNT)); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + assertThat(page.count(), is(RECORDS.size())); + assertThat(page.scannedCount(), is(RECORDS.size())); + assertThat(page.items(), is(empty())); + } + + @Test + public void queryNestedRecord_withMixedNestedAndTopLevelProjections_shouldProjectMultipleAttributes() { + insertNestedRecords(); + SdkPublisher> publisher = + mappedNestedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributesToProject(Arrays.asList( + NestedAttributeName.builder().elements("innerAttributeRecord", "attribOne").build())) + .addNestedAttributesToProject(NestedAttributeName.create("innerAttributeRecord", "attribTwo")) + .addAttributeToProject("sort")); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(1)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-1")); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(1)); + } + + @Test + public void queryRecords_withEmptyProjectionList_shouldReturnAllAttributes() { + insertNestedRecords(); + SdkPublisher> publisher = + mappedNestedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject(new ArrayList<>())); + List> results = drainPublisher(publisher, 1); + NestedTestRecord firstRecord = results.get(0).items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is("id-value-7")); + assertThat(firstRecord.getSort(), is(7)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(7)); + assertThat(firstRecord.getDotVariable(), is("v7")); + } + + @Test + public void queryRecords_withEmptyProjectionString_shouldThrowAssertionError() { + insertNestedRecords(); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> drainPublisher( + mappedNestedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value-3"))) + .attributesToProject("")), + 1)); + } + + @Test + public void queryRecords_withEmptyNestedProjectionName_shouldThrowAssertionError() { + insertNestedRecords(); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> drainPublisher( + mappedNestedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value-3"))) + .addNestedAttributeToProject(NestedAttributeName.create(""))), + 1)); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicScanTest.java index e69ff876e4ad..a5988c956173 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicScanTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBasicScanTest.java @@ -15,8 +15,10 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; @@ -24,6 +26,8 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -38,13 +42,17 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.Select; public class AsyncBasicScanTest extends LocalDynamoDbAsyncTestBase { private static class Record { @@ -102,20 +110,42 @@ public int hashCode() { .mapToObj(i -> new Record().setId("id-value").setSort(i)) .collect(Collectors.toList()); + private static final List NESTED_TEST_RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> { + NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-" + i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v" + i); + return nestedTestRecord; + }) + .collect(Collectors.toList()); + private DynamoDbEnhancedAsyncClient enhancedAsyncClient = DefaultDynamoDbEnhancedAsyncClient.builder() .dynamoDbClient(getDynamoDbAsyncClient()) .build(); private DynamoDbAsyncTable mappedTable = enhancedAsyncClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private DynamoDbAsyncTable mappedNestedTable = + enhancedAsyncClient.table(getConcreteTableName("nested-table-name"), TableSchema.fromClass(NestedTestRecord.class)); private void insertRecords() { RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record)).join()); } + private void insertNestedRecords() { + NESTED_TEST_RECORDS.forEach(record -> mappedNestedTable.putItem(r -> r.item(record)).join()); + } + @Before public void createTable() { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + mappedNestedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); } @After @@ -123,6 +153,9 @@ public void deleteTable() { getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() .tableName(getConcreteTableName("table-name")) .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("nested-table-name")) + .build()).join(); } @Test @@ -169,6 +202,40 @@ public void scanAllRecordsWithFilter() { assertThat(page.lastEvaluatedKey(), is(nullValue())); } + @Test + public void scanAllRecords_withAttributeProjection_shouldExcludeUnprojectedAttributes() { + insertRecords(); + SdkPublisher> publisher = mappedTable.scan(b -> b.attributesToProject("sort")); + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + assertThat(page.items().size(), is(RECORDS.size())); + Record firstRecord = page.items().get(0); + assertThat(firstRecord.id, is(nullValue())); + assertThat(firstRecord.sort, is(0)); + } + + @Test + public void scanRecords_withBothFilterAndProjection_shouldApplyBoth() { + insertRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + SdkPublisher> publisher = mappedTable.scan(ScanEnhancedRequest.builder() + .attributesToProject("sort") + .filterExpression(expression) + .build()); + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + assertThat(page.items(), hasSize(3)); + assertThat(page.items().get(0).id, is(nullValue())); + assertThat(page.items().get(0).sort, is(3)); + } + @Test public void scanAllRecordsWithFilter_viaItems() { insertRecords(); @@ -261,6 +328,88 @@ public void scanExclusiveStartKey_viaItems() { assertThat(results, is(RECORDS.subList(8, 10))); } + @Test + public void scanAllRecords_withSelectCount_shouldReturnCountNotItems() { + insertRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + SdkPublisher> publisher = mappedTable.scan(ScanEnhancedRequest.builder() + .select(Select.COUNT) + .filterExpression(expression) + .build()); + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + assertThat(page.count(), is(3)); + assertThat(page.items().size(), is(0)); + } + + @Test + public void scanRecords_withBothFilterAndNestedProjection_shouldApplyBoth() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + SdkPublisher> publisher = mappedNestedTable.scan(ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject( + NestedAttributeName.create(Arrays.asList("innerAttributeRecord", "attribOne"))) + .build()); + List> results = drainPublisher(publisher, 1); + assertThat(results.get(0).items().size(), is(3)); + } + + @Test + public void scanRecords_withEmptyNestedAttributeName_shouldThrowAssertionError() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> drainPublisher(mappedNestedTable.scan(ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder() + .elements("") + .build()) + .build()), + 1)); + } + + @Test + public void scanRecords_withEmptyTopLevelAttributeName_shouldThrowAssertionError() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> drainPublisher(mappedNestedTable.scan(ScanEnhancedRequest.builder() + .filterExpression(expression) + .addAttributeToProject("") + .build()), + 1)); + } + private Map getKeyMap(int sort) { Map result = new HashMap<>(); result.put("id", stringValue("id-value")); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchGetItemTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchGetItemTest.java index c22263f6e4e0..21ffed20859d 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchGetItemTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchGetItemTest.java @@ -38,6 +38,10 @@ import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.model.BatchGetItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.BatchGetResultPage; import software.amazon.awssdk.enhanced.dynamodb.model.BatchGetResultPagePublisher; @@ -47,6 +51,34 @@ import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; public class AsyncBatchGetItemTest extends LocalDynamoDbAsyncTestBase { + @DynamoDbBean + public static class BeanRecord { + private Integer id; + private String value; + @DynamoDbPartitionKey + public Integer getId() { return id; } + public void setId(Integer id) { this.id = id; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + } + + @DynamoDbImmutable(builder = ImmutableRecord.Builder.class) + public static final class ImmutableRecord { + private final Integer id; + private final String value; + private ImmutableRecord(Builder b) { this.id = b.id; this.value = b.value; } + @DynamoDbPartitionKey + public Integer id() { return id; } + public String value() { return value; } + public static Builder builder() { return new Builder(); } + public static final class Builder { + private Integer id; + private String value; + public Builder id(Integer id) { this.id = id; return this; } + public Builder value(String value) { this.value = value; return this; } + public ImmutableRecord build() { return new ImmutableRecord(this); } + } + } private static class Record1 { private Integer id; private String stringAttr; @@ -152,6 +184,10 @@ public int hashCode() { private final String tableName2 = getConcreteTableName("table-name-2"); private final DynamoDbAsyncTable mappedTable1 = enhancedAsyncClient.table(tableName1, TABLE_SCHEMA_1); private final DynamoDbAsyncTable mappedTable2 = enhancedAsyncClient.table(tableName2, TABLE_SCHEMA_2); + private final DynamoDbAsyncTable mappedBeanTable = + enhancedAsyncClient.table(getConcreteTableName("table-name-bean"), TableSchema.fromBean(BeanRecord.class)); + private final DynamoDbAsyncTable mappedImmutableTable = + enhancedAsyncClient.table(getConcreteTableName("table-name-immutable"), TableSchema.fromImmutableClass(ImmutableRecord.class)); private static final List RECORDS_1 = IntStream.range(0, 2) @@ -167,6 +203,8 @@ public int hashCode() { public void createTable() { mappedTable1.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); mappedTable2.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + mappedBeanTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + mappedImmutableTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); } @After @@ -177,6 +215,12 @@ public void deleteTable() { getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() .tableName(tableName2) .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-bean")) + .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-immutable")) + .build()).join(); } private void insertRecords() { @@ -312,6 +356,74 @@ public void notFoundRecords_withReturnConsumedCapacity() { } + @Test + public void batchGetRecordsWithExtension_shouldPreserveAndReturnCorrectData() { + insertRecords(); + + DynamoDbEnhancedAsyncClient extensionClient = + DefaultDynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + + DynamoDbAsyncTable extensionMappedTable1 = extensionClient.table(tableName1, TABLE_SCHEMA_1); + DynamoDbAsyncTable extensionMappedTable2 = extensionClient.table(tableName2, TABLE_SCHEMA_2); + + BatchGetItemEnhancedRequest request = BatchGetItemEnhancedRequest.builder() + .readBatches( + ReadBatch.builder(Record1.class) + .mappedTableResource(extensionMappedTable1) + .addGetItem(i -> i.key(k -> k.partitionValue(0))) + .build(), + ReadBatch.builder(Record2.class) + .mappedTableResource(extensionMappedTable2) + .addGetItem(i -> i.key(k -> k.partitionValue(0))) + .build(), + ReadBatch.builder(Record2.class) + .mappedTableResource(extensionMappedTable2) + .addGetItem(i -> i.key(k -> k.partitionValue(1))) + .build(), + ReadBatch.builder(Record1.class) + .mappedTableResource(extensionMappedTable1) + .addGetItem(i -> i.key(k -> k.partitionValue(1))) + .build()) + .build(); + + BatchGetResultPagePublisher publisher = extensionClient.batchGetItem(request); + List results = drainPublisher(publisher, 1); + assertThat(results.size(), is(1)); + + BatchGetResultPage page = results.get(0); + List record1List = page.resultsForTable(extensionMappedTable1); + assertThat(record1List.size(), is(2)); + assertThat(record1List, containsInAnyOrder(RECORDS_1.get(0), RECORDS_1.get(1))); + + List record2List = page.resultsForTable(extensionMappedTable2); + assertThat(record2List.size(), is(2)); + assertThat(record2List, containsInAnyOrder(RECORDS_2.get(0), RECORDS_2.get(1))); + } + + @Test + public void batchGetRecords_fromMixedSchemaTypes_shouldReturnCorrectlyCastItems() { + insertRecords(); + BeanRecord bean = new BeanRecord(); + bean.setId(10); + bean.setValue("bean"); + mappedBeanTable.putItem(bean).join(); + ImmutableRecord immutable = ImmutableRecord.builder().id(20).value("immutable").build(); + mappedImmutableTable.putItem(immutable).join(); + + BatchGetResultPagePublisher publisher = enhancedAsyncClient.batchGetItem(r -> r.readBatches( + ReadBatch.builder(Record1.class).mappedTableResource(mappedTable1).addGetItem(i -> i.key(k -> k.partitionValue(0))).build(), + ReadBatch.builder(BeanRecord.class).mappedTableResource(mappedBeanTable).addGetItem(i -> i.key(k -> k.partitionValue(10))).build(), + ReadBatch.builder(ImmutableRecord.class).mappedTableResource(mappedImmutableTable).addGetItem(i -> i.key(k -> k.partitionValue(20))).build())); + + List pages = drainPublisher(publisher, 1); + assertThat(pages.size(), is(1)); + assertThat(pages.get(0).resultsForTable(mappedBeanTable).get(0).getValue(), is("bean")); + assertThat(pages.get(0).resultsForTable(mappedImmutableTable).get(0).value(), is("immutable")); + } + private BatchGetItemEnhancedRequest requestWithNotFoundRecord() { return BatchGetItemEnhancedRequest.builder() .readBatches( diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchWriteItemTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchWriteItemTest.java index 79df622d9a82..3f4ad819c98e 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchWriteItemTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncBatchWriteItemTest.java @@ -19,9 +19,14 @@ import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -35,6 +40,8 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteItemEnhancedRequest; @@ -42,12 +49,36 @@ import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ItemCollectionMetrics; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics; public class AsyncBatchWriteItemTest extends LocalDynamoDbAsyncTestBase { + private static class TimestampedRecord { + private String id; + private Instant time; + + private String getId() { + return id; + } + + private TimestampedRecord setId(String id) { + this.id = id; + return this; + } + + private Instant getTime() { + return time; + } + + private TimestampedRecord setTime(Instant time) { + this.time = time; + return this; + } + } + private static class Record1 { private Integer id; private String attribute; @@ -154,15 +185,37 @@ public int hashCode() { .setter(Record2::setAttribute)) .build(); + private static final TableSchema TIMESTAMPED_TABLE_SCHEMA = + StaticTableSchema.builder(TimestampedRecord.class) + .newItemSupplier(TimestampedRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TimestampedRecord::getId) + .setter(TimestampedRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(Instant.class, a -> a.name("time") + .getter(TimestampedRecord::getTime) + .setter(TimestampedRecord::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + private DynamoDbEnhancedAsyncClient enhancedAsyncClient = DefaultDynamoDbEnhancedAsyncClient.builder() .dynamoDbClient(getDynamoDbAsyncClient()) .build(); + private DynamoDbEnhancedAsyncClient extensionEnhancedAsyncClient = + DefaultDynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); private DynamoDbAsyncTable mappedTable1 = enhancedAsyncClient.table(getConcreteTableName("table-name-1"), TABLE_SCHEMA_1); private DynamoDbAsyncTable mappedTable2 = enhancedAsyncClient.table(getConcreteTableName("table-name-2"), TABLE_SCHEMA_2); + private DynamoDbAsyncTable timestampedTable = + extensionEnhancedAsyncClient.table(getConcreteTableName("timestamped-table"), TIMESTAMPED_TABLE_SCHEMA); + private DynamoDbAsyncTable> unsupportedCustomSchemaTable = + extensionEnhancedAsyncClient.table(getConcreteTableName("unsupported-schema-table"), new UnsupportedConverterTableSchema()); private static final List RECORDS_1 = IntStream.range(0, 2) @@ -178,6 +231,8 @@ public int hashCode() { public void createTable() { mappedTable1.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); mappedTable2.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + timestampedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + unsupportedCustomSchemaTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); } @After @@ -188,6 +243,12 @@ public void deleteTable() { getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() .tableName(getConcreteTableName("table-name-2")) .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("timestamped-table")) + .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("unsupported-schema-table")) + .build()).join(); } @Test @@ -419,4 +480,75 @@ public void mixedCommands_withConsumedCapacity() { assertThat(mappedTable2.getItem(r -> r.key(k -> k.partitionValue(0))).join(), is(nullValue())); } + @Test + public void batchWrite_withSupportedAndUnsupportedSchema_shouldPreserveUnsupportedSchemaValues() { + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("payload", + AttributeValue.builder().m(Collections.singletonMap("inner", + AttributeValue.builder().s("value").build())) + .build()); + + extensionEnhancedAsyncClient.batchWriteItem( + BatchWriteItemEnhancedRequest.builder() + .writeBatches( + WriteBatch.builder(TimestampedRecord.class) + .mappedTableResource(timestampedTable) + .addPutItem(r -> r.item(new TimestampedRecord().setId("ts-1"))) + .build(), + WriteBatch.builder(customTableType()) + .mappedTableResource(unsupportedCustomSchemaTable) + .addPutItem(r -> r.item(customItem)) + .build()) + .build()).join(); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))).join(); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))).join(); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("payload").m().get("inner").s(), is("value")); + } + + @Test + public void batchWriteWithResponse_withSupportedAndUnsupportedSchema_shouldReturnMetrics() { + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("payload", + AttributeValue.builder().m(Collections.singletonMap("inner", + AttributeValue.builder().s("value").build())) + .build()); + + BatchWriteResult result = + extensionEnhancedAsyncClient.batchWriteItem(BatchWriteItemEnhancedRequest.builder() + .writeBatches( + WriteBatch.builder(TimestampedRecord.class) + .mappedTableResource(timestampedTable) + .addPutItem(r -> r.item(new TimestampedRecord().setId("ts-1"))) + .build(), + WriteBatch.builder(customTableType()) + .mappedTableResource(unsupportedCustomSchemaTable) + .addPutItem(r -> r.item(customItem)) + .build()) + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .build()).join(); + + assertThat(result.consumedCapacity(), is(notNullValue())); + assertThat(result.consumedCapacity().size(), is(2)); + assertThat(result.itemCollectionMetrics(), is(notNullValue())); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))).join(); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))).join(); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("payload").m().get("inner").s(), is("value")); + } + + @SuppressWarnings("unchecked") + private Class> customTableType() { + return (Class>) (Class) Map.class; + } + } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncDeleteItemWithResponseTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncDeleteItemWithResponseTest.java index d4c9c2492800..ac2b4fe9fef9 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncDeleteItemWithResponseTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncDeleteItemWithResponseTest.java @@ -116,4 +116,20 @@ public void returnConsumedCapacity_set_consumedCapacityNotNull() { assertThat(response.consumedCapacity()).isNotNull(); } + + @Test + public void deleteItem_simpleAndWithResponse_shouldReturnSameAttributes() { + Record first = new Record().setId(11).setStringAttr1("a"); + mappedTable1.putItem(first).join(); + Record deletedBySimple = mappedTable1.deleteItem(r -> r.key(k -> k.partitionValue(11))).join(); + assertThat(deletedBySimple).isEqualTo(first); + assertThat(mappedTable1.getItem(r -> r.key(k -> k.partitionValue(11))).join()).isNull(); + + Record second = new Record().setId(12).setStringAttr1("b"); + mappedTable1.putItem(second).join(); + DeleteItemEnhancedResponse response = + mappedTable1.deleteItemWithResponse(r -> r.key(k -> k.partitionValue(12))).join(); + assertThat(response.attributes()).isEqualTo(second); + assertThat(mappedTable1.getItem(r -> r.key(k -> k.partitionValue(12))).join()).isNull(); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncGetItemWithResponseTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncGetItemWithResponseTest.java index 9581233810fa..2f2011535de8 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncGetItemWithResponseTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncGetItemWithResponseTest.java @@ -27,6 +27,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse; import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; @@ -152,6 +153,22 @@ public void getItemWithResponse_withReturnConsumedCapacity_when_recordIsPresent_ assertThat(consumedCapacity.capacityUnits()).isCloseTo(25.0, Offset.offset(0.01)); } + @Test + public void getItem_withExtension_simpleAndWithResponse_shouldReturnSameData() { + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable extensionTable = extensionClient.table(getConcreteTableName(TEST_TABLE_NAME), TABLE_SCHEMA); + Record original = new Record().setId(777).setStringAttr1("abc"); + extensionTable.putItem(original).join(); + + Record getResult = extensionTable.getItem(r -> r.key(k -> k.partitionValue(777))).join(); + GetItemEnhancedResponse response = + extensionTable.getItemWithResponse(r -> r.key(k -> k.partitionValue(777))).join(); + assertThat(getResult).isEqualTo(response.attributes()); + } + private static String getStringAttrValue(int numChars) { char[] chars = new char[numChars]; Arrays.fill(chars, 'a'); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexQueryTest.java index 0a53549e6f62..b8f658493652 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexQueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexQueryTest.java @@ -17,7 +17,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; @@ -52,6 +55,7 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; public class AsyncIndexQueryTest extends LocalDynamoDbAsyncTestBase { private static class Record { @@ -230,6 +234,26 @@ public void queryBetween() { assertThat(page.lastEvaluatedKey(), is(nullValue())); } + @Test + public void queryBetween_withConsumedCapacity_shouldReturnIndexMetrics() { + insertRecords(); + Key fromKey = Key.builder().partitionValue("gsi-id-value").sortValue(3).build(); + Key toKey = Key.builder().partitionValue("gsi-id-value").sortValue(5).build(); + SdkPublisher> publisher = + keysOnlyMappedIndex.query(r -> r.queryConditional(QueryConditional.sortBetween(fromKey, toKey)) + .returnConsumedCapacity(ReturnConsumedCapacity.INDEXES)); + + Page page = drainPublisher(publisher, 1).get(0); + assertThat(page.items(), + is(KEYS_ONLY_RECORDS.stream().filter(r -> r.sort >= 3 && r.sort <= 5).collect(Collectors.toList()))); + assertThat(page.consumedCapacity(), is(notNullValue())); + assertThat(page.consumedCapacity().globalSecondaryIndexes(), is(notNullValue())); + assertThat(page.consumedCapacity().globalSecondaryIndexes().isEmpty(), is(false)); + assertThat(page.consumedCapacity().capacityUnits(), is(greaterThan(0d))); + assertThat(page.count(), equalTo(3)); + assertThat(page.scannedCount(), equalTo(3)); + } + @Test public void queryLimit() { insertRecords(); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexScanTest.java index 9de11ea5c0fd..2c28f41595da 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexScanTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncIndexScanTest.java @@ -17,7 +17,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; @@ -50,6 +52,7 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; public class AsyncIndexScanTest extends LocalDynamoDbAsyncTestBase { private static class Record { @@ -208,6 +211,19 @@ public void scanAllRecordsDefaultSettings() { assertThat(page.lastEvaluatedKey(), is(nullValue())); } + @Test + public void scanAllRecords_withConsumedCapacity_shouldReturnIndexMetrics() { + insertRecords(); + SdkPublisher> publisher = + keysOnlyMappedIndex.scan(r -> r.returnConsumedCapacity(ReturnConsumedCapacity.INDEXES)); + Page page = drainPublisher(publisher, 1).get(0); + assertThat(page.items(), is(KEYS_ONLY_RECORDS)); + assertThat(page.consumedCapacity(), is(notNullValue())); + assertThat(page.consumedCapacity().globalSecondaryIndexes(), is(notNullValue())); + assertThat(page.count(), equalTo(10)); + assertThat(page.scannedCount(), equalTo(10)); + } + @Test public void scanAllRecordsWithFilter() { insertRecords(); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncPutItemWithResponseTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncPutItemWithResponseTest.java index 275aea9106ed..e15688705941 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncPutItemWithResponseTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncPutItemWithResponseTest.java @@ -16,15 +16,18 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import java.util.Objects; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; @@ -162,4 +165,30 @@ public void returnItemCollectionMetrics_unset_itemCollectionMetricsNull() { assertThat(response.consumedCapacity()).isNull(); } + + @Test + public void putItemWithResponse_returnAllOld_shouldInvokeExtensionOnReturnedValues() { + DynamoDbEnhancedClientExtension extension = Mockito.spy(new NoOpExtension()); + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(extension) + .build(); + DynamoDbAsyncTable extensionTable = + extensionClient.table(getConcreteTableName("table-name-extension"), TABLE_SCHEMA); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + Record original = new Record().setId(77).setStringAttr1("a"); + extensionTable.putItem(original).join(); + Record update = new Record().setId(77).setStringAttr1("b"); + extensionTable.putItemWithResponse(r -> r.returnValues(ReturnValue.ALL_OLD).item(update)).join(); + + Mockito.verify(extension, Mockito.times(1)).afterRead(any(DynamoDbExtensionContext.AfterRead.class)); + Mockito.verify(extension, Mockito.times(2)).beforeWrite(any(DynamoDbExtensionContext.BeforeWrite.class)); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-extension")) + .build()).join(); + } + + private static class NoOpExtension implements DynamoDbEnhancedClientExtension { + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactGetItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactGetItemsTest.java index 18c2466cddab..8f2b2407fb2c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactGetItemsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactGetItemsTest.java @@ -32,6 +32,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.TransactGetItemsEnhancedRequest; @@ -190,5 +191,34 @@ public void notFoundRecordReturnsNull() { assertThat(results.get(2).getItem(mappedTable2), is(nullValue())); assertThat(results.get(3).getItem(mappedTable1), is(RECORDS_1.get(1))); } + + @Test + public void transactGetRecords_withExtension_shouldPreserveAndReturnCorrectData() { + insertRecords(); + + DynamoDbEnhancedAsyncClient extensionClient = DefaultDynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable extensionMappedTable1 = + extensionClient.table(getConcreteTableName("table-name-1"), TABLE_SCHEMA_1); + DynamoDbAsyncTable extensionMappedTable2 = + extensionClient.table(getConcreteTableName("table-name-2"), TABLE_SCHEMA_2); + + TransactGetItemsEnhancedRequest request = + TransactGetItemsEnhancedRequest.builder() + .addGetItem(extensionMappedTable1, Key.builder().partitionValue(0).build()) + .addGetItem(extensionMappedTable2, Key.builder().partitionValue(0).build()) + .addGetItem(extensionMappedTable2, Key.builder().partitionValue(1).build()) + .addGetItem(extensionMappedTable1, Key.builder().partitionValue(1).build()) + .build(); + + List results = extensionClient.transactGetItems(request).join(); + assertThat(results.size(), is(4)); + assertThat(results.get(0).getItem(extensionMappedTable1), is(RECORDS_1.get(0))); + assertThat(results.get(1).getItem(extensionMappedTable2), is(RECORDS_2.get(0))); + assertThat(results.get(2).getItem(extensionMappedTable2), is(RECORDS_2.get(1))); + assertThat(results.get(3).getItem(extensionMappedTable1), is(RECORDS_1.get(1))); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactWriteItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactWriteItemsTest.java index f2be7ca4cbc1..03cee3a837b9 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactWriteItemsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncTransactWriteItemsTest.java @@ -21,11 +21,15 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; import static org.junit.Assert.fail; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import java.time.Instant; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletionException; import java.util.stream.Collectors; @@ -38,17 +42,48 @@ import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.ConditionCheck; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactDeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactPutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactUpdateItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedResponse; +import software.amazon.awssdk.services.dynamodb.model.CancellationReason; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; public class AsyncTransactWriteItemsTest extends LocalDynamoDbAsyncTestBase { + private static class TimestampedRecord { + private String id; + private Instant time; + + private String getId() { + return id; + } + + private TimestampedRecord setId(String id) { + this.id = id; + return this; + } + + private Instant getTime() { + return time; + } + + private TimestampedRecord setTime(Instant time) { + this.time = time; + return this; + } + } + private static class Record1 { private Integer id; private String attribute; @@ -147,15 +182,37 @@ public int hashCode() { .setter(Record2::setAttribute)) .build(); + private static final TableSchema TIMESTAMPED_TABLE_SCHEMA = + StaticTableSchema.builder(TimestampedRecord.class) + .newItemSupplier(TimestampedRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TimestampedRecord::getId) + .setter(TimestampedRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(Instant.class, a -> a.name("time") + .getter(TimestampedRecord::getTime) + .setter(TimestampedRecord::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + private DynamoDbEnhancedAsyncClient enhancedAsyncClient = DefaultDynamoDbEnhancedAsyncClient.builder() .dynamoDbClient(getDynamoDbAsyncClient()) .build(); + private DynamoDbEnhancedAsyncClient extensionEnhancedAsyncClient = + DefaultDynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); private DynamoDbAsyncTable mappedTable1 = enhancedAsyncClient.table(getConcreteTableName("table-name-1"), TABLE_SCHEMA_1); private DynamoDbAsyncTable mappedTable2 = enhancedAsyncClient.table(getConcreteTableName("table-name-2"), TABLE_SCHEMA_2); + private DynamoDbAsyncTable timestampedTable = + extensionEnhancedAsyncClient.table(getConcreteTableName("timestamped-table"), TIMESTAMPED_TABLE_SCHEMA); + private DynamoDbAsyncTable> unsupportedCustomSchemaTable = + extensionEnhancedAsyncClient.table(getConcreteTableName("unsupported-schema-table"), new UnsupportedConverterTableSchema()); private static final List RECORDS_1 = IntStream.range(0, 2) @@ -171,6 +228,8 @@ public int hashCode() { public void createTable() { mappedTable1.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); mappedTable2.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + timestampedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + unsupportedCustomSchemaTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); } @After @@ -181,6 +240,12 @@ public void deleteTable() { getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() .tableName(getConcreteTableName("table-name-2")) .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("timestamped-table")) + .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("unsupported-schema-table")) + .build()).join(); } @Test @@ -429,5 +494,107 @@ public void mixedCommands_conditionCheckFailsTransaction() { assertThat(mappedTable2.getItem(r -> r.key(k -> k.partitionValue(0))).join(), is(RECORDS_2.get(0))); assertThat(mappedTable2.getItem(r -> r.key(k -> k.partitionValue(1))).join(), is(nullValue())); } + + @Test + public void transactWrite_withConditionCheckFailure_shouldReturnAllOldValues() { + mappedTable1.putItem(r -> r.item(RECORDS_1.get(0))).join(); + mappedTable1.putItem(r -> r.item(RECORDS_1.get(1))).join(); + mappedTable2.putItem(r -> r.item(RECORDS_2.get(0))).join(); + + Expression conditionExpression = Expression.builder() + .expression("#attribute = :attribute") + .expressionValues(singletonMap(":attribute", stringValue("99"))) + .expressionNames(singletonMap("#attribute", "attribute")) + .build(); + + Key key0 = Key.builder().partitionValue(0).build(); + Key key1 = Key.builder().partitionValue(1).build(); + ReturnValuesOnConditionCheckFailure returnValues = ReturnValuesOnConditionCheckFailure.ALL_OLD; + + TransactPutItemEnhancedRequest putItemRequest = TransactPutItemEnhancedRequest.builder(Record2.class) + .conditionExpression(conditionExpression) + .item(RECORDS_2.get(0)) + .returnValuesOnConditionCheckFailure(returnValues) + .build(); + TransactUpdateItemEnhancedRequest updateItemRequest = TransactUpdateItemEnhancedRequest.builder(Record1.class) + .conditionExpression(conditionExpression) + .item(RECORDS_1.get(0)) + .returnValuesOnConditionCheckFailure(returnValues) + .build(); + TransactDeleteItemEnhancedRequest deleteItemRequest = TransactDeleteItemEnhancedRequest.builder() + .key(key1) + .conditionExpression(conditionExpression) + .returnValuesOnConditionCheckFailure(returnValues) + .build(); + + TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable2, putItemRequest) + .addUpdateItem(mappedTable1, updateItemRequest) + .addConditionCheck(mappedTable1, ConditionCheck.builder() + .key(key0) + .conditionExpression(conditionExpression) + .returnValuesOnConditionCheckFailure(returnValues) + .build()) + .addDeleteItem(mappedTable1, deleteItemRequest) + .build(); + + try { + enhancedAsyncClient.transactWriteItems(request).join(); + fail("Expected CompletionException to be thrown"); + } catch (CompletionException e) { + assertThat(e.getCause(), instanceOf(TransactionCanceledException.class)); + List reasons = ((TransactionCanceledException) e.getCause()).cancellationReasons(); + assertThat(reasons.size(), is(4)); + reasons.forEach(r -> assertThat(r.item().isEmpty(), is(false))); + } + } + + @Test + public void transactWrite_withSupportedAndUnsupportedSchema_shouldPreserveValues() { + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("payload", + AttributeValue.builder().m(singletonMap("inner", AttributeValue.builder().s("value").build())).build()); + + extensionEnhancedAsyncClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(timestampedTable, new TimestampedRecord().setId("ts-1")) + .addPutItem(unsupportedCustomSchemaTable, customItem) + .build()).join(); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))).join(); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))).join(); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("payload").m().get("inner").s(), is("value")); + } + + @Test + public void transactWriteWithResponse_withSupportedAndUnsupportedSchema_shouldReturnMetrics() { + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("payload", + AttributeValue.builder().m(singletonMap("inner", AttributeValue.builder().s("value").build())).build()); + + TransactWriteItemsEnhancedResponse response = extensionEnhancedAsyncClient.transactWriteItemsWithResponse( + TransactWriteItemsEnhancedRequest.builder() + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .addPutItem(timestampedTable, new TimestampedRecord().setId("ts-1")) + .addPutItem(unsupportedCustomSchemaTable, customItem) + .build()).join(); + + assertThat(response.consumedCapacity(), is(notNullValue())); + assertThat(response.consumedCapacity().size(), is(2)); + assertThat(response.itemCollectionMetrics(), is(notNullValue())); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))).join(); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))).join(); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("payload").m().get("inner").s(), is("value")); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncUpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncUpdateBehaviorTest.java new file mode 100644 index 000000000000..0566827d92ca --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncUpdateBehaviorTest.java @@ -0,0 +1,66 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; +import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; + +public class AsyncUpdateBehaviorTest extends LocalDynamoDbAsyncTestBase { + private static final Instant INSTANT_1 = Instant.parse("2020-05-03T10:00:00Z"); + private static final Instant INSTANT_2 = Instant.parse("2020-05-03T10:05:00Z"); + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(Stream.concat( + ExtensionResolver.defaultExtensions().stream(), + Stream.of(AutoGeneratedTimestampRecordExtension.create())) + .collect(Collectors.toList())) + .build(); + + private final DynamoDbAsyncTable mappedTable = + enhancedAsyncClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))).join(); + } + + @Test + public void updateItem_withWriteIfNotExistsBehavior_shouldPreserveOnSecondUpdateAndIncrementVersion() { + RecordWithUpdateBehaviors seed = new RecordWithUpdateBehaviors(); + seed.setId("id1"); + seed.setCreatedOn(INSTANT_1); + seed.setLastUpdatedOn(INSTANT_1); + mappedTable.updateItem(seed).join(); + + RecordWithUpdateBehaviors update = new RecordWithUpdateBehaviors(); + update.setId("id1"); + update.setVersion(1L); + update.setCreatedOn(INSTANT_2); + update.setLastUpdatedOn(INSTANT_2); + mappedTable.updateItem(update).join(); + + RecordWithUpdateBehaviors persisted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); + assertThat(persisted.getVersion()).isEqualTo(2L); + assertThat(persisted.getCreatedOn()).as("WRITE_IF_NOT_EXISTS should preserve created time").isEqualTo(INSTANT_1); + assertThat(persisted.getLastUpdatedOn()).isEqualTo(INSTANT_2); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncUpdateExpressionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncUpdateExpressionTest.java new file mode 100644 index 000000000000..69b131b0e726 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncUpdateExpressionTest.java @@ -0,0 +1,57 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordForUpdateExpressions; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class AsyncUpdateExpressionTest extends LocalDynamoDbAsyncTestBase { + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(RecordForUpdateExpressions.class); + + private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = + DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build(); + + private final DynamoDbAsyncTable mappedTable = + enhancedAsyncClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))).join(); + } + + @Test + public void updateItem_withConditionExpression_shouldNotApplyUpdateWhenConditionMatches() { + RecordForUpdateExpressions seed = new RecordForUpdateExpressions(); + seed.setId("id1"); + seed.setStringAttribute("init"); + mappedTable.putItem(seed).join(); + + RecordForUpdateExpressions update = new RecordForUpdateExpressions(); + update.setId("id1"); + update.setStringAttribute("changed"); + mappedTable.updateItem(r -> r.item(update) + .conditionExpression(Expression.builder() + .expression("id = :id") + .expressionValues(Collections.singletonMap(":id", + AttributeValue.builder().s("id1").build())) + .build())) + .join(); + + RecordForUpdateExpressions persisted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); + assertThat(persisted.getStringAttribute()).isEqualTo("init"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncVersionedRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncVersionedRecordTest.java new file mode 100644 index 000000000000..da862879e025 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncVersionedRecordTest.java @@ -0,0 +1,78 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; + +public class AsyncVersionedRecordTest extends LocalDynamoDbAsyncTestBase { + private static class Record { + private String id; + private String attribute; + private Integer version; + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getAttribute() { return attribute; } + public void setAttribute(String attribute) { this.attribute = attribute; } + public Integer getVersion() { return version; } + public void setVersion(Integer version) { this.version = version; } + } + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, a -> a.name("id").getter(Record::getId).setter(Record::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attribute").getter(Record::getAttribute) + .setter(Record::setAttribute)) + .addAttribute(Integer.class, a -> a.name("version").getter(Record::getVersion) + .setter(Record::setVersion).tags(versionAttribute())) + .build(); + + private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = + DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(VersionedRecordExtension.builder().build()) + .build(); + + private final DynamoDbAsyncTable mappedTable = enhancedAsyncClient.table(getConcreteTableName("table-name"), + TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))).join(); + } + + @Test + public void transactWrite_updateVersionedRecord_shouldIncrementVersion() { + Record record = new Record(); + record.setId("id1"); + record.setAttribute("v1"); + mappedTable.updateItem(record).join(); + Record persistedAfterFirstUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); + assertThat(persistedAfterFirstUpdate.getVersion()).isEqualTo(1); + + Record update = new Record(); + update.setId("id1"); + update.setAttribute("v2"); + update.setVersion(1); + enhancedAsyncClient.transactWriteItems(r -> r.addUpdateItem(mappedTable, update)).join(); + + Record persisted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); + assertThat(persisted.getVersion()).isEqualTo(persistedAfterFirstUpdate.getVersion() + 1); + assertThat(persisted.getAttribute()).isEqualTo("v2"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java index 9b3d12e6d55f..6de7776cceb7 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import java.util.stream.IntStream; import org.junit.After; @@ -24,8 +25,10 @@ import org.junit.Test; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AtomicCounterRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; @@ -194,4 +197,32 @@ public void batchPut_initializesCorrectly() { assertThat(persistedRecord.getAttribute1()).isEqualTo(STRING_VALUE); assertThat(persistedRecord.getDefaultCounter()).isEqualTo(1L); } + + @Test + public void updateItem_withConditionExpression_shouldIncrementCountersAndPreserveAttribute() { + AtomicCounterRecord record = new AtomicCounterRecord(); + record.setId(RECORD_ID); + record.setAttribute1("initial"); + mappedTable.putItem(record); + + Expression expression = Expression.builder() + .expression("#a = :v") + .putExpressionName("#a", "attribute1") + .putExpressionValue(":v", stringValue("initial")) + .build(); + AtomicCounterRecord update = new AtomicCounterRecord(); + update.setId(RECORD_ID); + update.setAttribute1("updated"); + + mappedTable.updateItem(UpdateItemEnhancedRequest.builder(AtomicCounterRecord.class) + .item(update) + .conditionExpression(expression) + .build()); + + AtomicCounterRecord persisted = mappedTable.getItem(update); + assertThat(persisted.getAttribute1()).isEqualTo("updated"); + assertThat(persisted.getDefaultCounter()).isEqualTo(1L); + assertThat(persisted.getCustomCounter()).isEqualTo(15L); + assertThat(persisted.getDecreasingCounter()).isEqualTo(-21L); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java index 52a14dbf65a6..84595e4427ef 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java @@ -91,6 +91,7 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; @@ -1564,6 +1565,232 @@ public void beforeWrite_customTableSchemaWithoutConverterForAttribute_doesNotThr assertThat(modification.transformedItem(), is(nullValue())); } + @Test + public void putItem_withUnsupportedCustomSchemaAndExtensionEnabled_writesWithoutTransformingNestedData() { + TableSchema> schema = new UnsupportedConverterTableSchema(); + DynamoDbTable> customTable = + enhancedClient.table(getConcreteTableName("custom-schema-table"), schema); + customTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Map nestedMap = new HashMap<>(); + nestedMap.put("city", AttributeValue.builder().s("Seattle").build()); + nestedMap.put("country", AttributeValue.builder().s("US").build()); + + Map item = new HashMap<>(); + item.put("pk", AttributeValue.builder().s("custom-1").build()); + item.put("location", AttributeValue.builder().m(nestedMap).build()); + + customTable.putItem(item); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))); + assertThat(persisted.get("location").m(), is(nestedMap)); + customTable.deleteTable(); + } + + @Test + public void updateItem_withUnsupportedCustomSchemaAndNestedList_writesWithoutThrowing() { + TableSchema> schema = new UnsupportedConverterTableSchema(); + DynamoDbTable> customTable = + enhancedClient.table(getConcreteTableName("custom-schema-table"), schema); + customTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Map nestedMap = new HashMap<>(); + nestedMap.put("child", AttributeValue.builder().s("one").build()); + + Map item = new HashMap<>(); + item.put("pk", AttributeValue.builder().s("custom-2").build()); + item.put("records", AttributeValue.builder() + .l(Arrays.asList(AttributeValue.builder().m(nestedMap).build(), + AttributeValue.builder().s("literal").build())) + .build()); + + customTable.updateItem(item); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-2"))); + assertThat(persisted.get("records").l().get(0).m(), is(nestedMap)); + assertThat(persisted.get("records").l().get(1).s(), is("literal")); + customTable.deleteTable(); + } + + @Test + public void putItem_withoutConverterForAttribute_doesNotThrow_nestedTimestampsSkipped() { + TableSchema supportedSchema = + buildStaticImmutableSchemaForNestedRecordWithList(); + TableSchema unsupportedConverterSchema = + new DelegatingTableSchemaWithoutConverterForAttribute<>(supportedSchema); + + DynamoDbTable table = + enhancedClient.table(getConcreteTableName("unsupported-converter-table"), unsupportedConverterSchema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Instant rootTimeBefore = MOCKED_INSTANT_UPDATE_ONE; + Instant nestedTimeBefore = MOCKED_INSTANT_UPDATE_TWO; + + NestedImmutableRecordWithList item = NestedImmutableRecordWithList + .builder() + .id("1") + .time(rootTimeBefore) + .level2(NestedImmutableChildRecordWithList.builder().time(nestedTimeBefore).build()) + .build(); + + table.putItem(r -> r.item(item)); + + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // When converterForAttribute cannot be resolved, the extension skips timestamp insertion. + assertThat(result.getTime(), is(rootTimeBefore)); + assertThat(result.getLevel2().getTime(), is(nestedTimeBefore)); + + table.deleteTable(); + } + + @Test + public void updateItem_withoutConverterForAttribute_doesNotThrow_nestedTimestampsSkipped() { + TableSchema supportedSchema = + buildStaticImmutableSchemaForNestedRecordWithList(); + TableSchema unsupportedConverterSchema = + new DelegatingTableSchemaWithoutConverterForAttribute<>(supportedSchema); + + DynamoDbTable table = + enhancedClient.table(getConcreteTableName("unsupported-converter-table"), unsupportedConverterSchema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Instant rootTimeBefore = MOCKED_INSTANT_UPDATE_ONE; + Instant nestedTimeBefore = MOCKED_INSTANT_UPDATE_TWO; + + NestedImmutableRecordWithList initial = NestedImmutableRecordWithList + .builder() + .id("1") + .time(rootTimeBefore) + .level2(NestedImmutableChildRecordWithList.builder().time(nestedTimeBefore).build()) + .build(); + + table.putItem(r -> r.item(initial)); + + NestedImmutableRecordWithList update = NestedImmutableRecordWithList + .builder() + .id("1") + .time(rootTimeBefore) + .level2(NestedImmutableChildRecordWithList.builder().time(nestedTimeBefore).build()) + .build(); + + table.updateItem(r -> r.item(update)); + + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getTime(), is(rootTimeBefore)); + assertThat(result.getLevel2().getTime(), is(nestedTimeBefore)); + + table.deleteTable(); + } + + @Test + public void putItem_converterForAttributeReturnsNull_doesNotThrow_nestedTimestampsSkipped() { + TableSchema supportedSchema = + buildStaticImmutableSchemaForNestedRecordWithList(); + TableSchema nullConverterSchema = + new DelegatingTableSchemaWithNullConverterForAttribute<>(supportedSchema); + + DynamoDbTable table = + enhancedClient.table(getConcreteTableName("null-converter-table"), nullConverterSchema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Instant rootTimeBefore = MOCKED_INSTANT_UPDATE_ONE; + Instant nestedTimeBefore = MOCKED_INSTANT_UPDATE_TWO; + + NestedImmutableRecordWithList item = NestedImmutableRecordWithList + .builder() + .id("1") + .time(rootTimeBefore) + .level2(NestedImmutableChildRecordWithList.builder().time(nestedTimeBefore).build()) + .build(); + + table.putItem(r -> r.item(item)); + + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getTime(), is(rootTimeBefore)); + assertThat(result.getLevel2().getTime(), is(nestedTimeBefore)); + + table.deleteTable(); + } + + @Test + public void sharedClient_cacheIsolation_afterUnsupportedConverterForAttribute_thenSupportedSchemaAppliesNestedTimestamps() { + TableSchema supportedSchema = + buildStaticImmutableSchemaForNestedRecordWithList(); + TableSchema unsupportedConverterSchema = + new DelegatingTableSchemaWithoutConverterForAttribute<>(supportedSchema); + + String tableName = getConcreteTableName("cache-isolation-table"); + + DynamoDbTable unsupportedTable = + enhancedClient.table(tableName, unsupportedConverterSchema); + unsupportedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Instant rootTimeBefore = MOCKED_INSTANT_UPDATE_ONE; + Instant nestedTimeBefore = MOCKED_INSTANT_UPDATE_TWO; + + NestedImmutableRecordWithList unsupportedItem = NestedImmutableRecordWithList + .builder() + .id("1") + .time(rootTimeBefore) + .level2(NestedImmutableChildRecordWithList.builder().time(nestedTimeBefore).build()) + .build(); + + unsupportedTable.putItem(r -> r.item(unsupportedItem)); + + DynamoDbTable supportedTable = + enhancedClient.table(tableName, supportedSchema); + + // Second operation should apply nested timestamps even after a previous operation failed to resolve converters. + Instant nestedTimeBefore2 = MOCKED_INSTANT_UPDATE_ONE; + NestedImmutableRecordWithList supportedItem = NestedImmutableRecordWithList + .builder() + .id("1") + .time(rootTimeBefore) + .level2(NestedImmutableChildRecordWithList.builder().time(nestedTimeBefore2).build()) + .build(); + + supportedTable.putItem(r -> r.item(supportedItem)); + + NestedImmutableRecordWithList result = supportedTable.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + + unsupportedTable.deleteTable(); + } + + @Test + public void deleteItem_withoutConverterForAttribute_doesNotThrow() { + TableSchema supportedSchema = + buildStaticImmutableSchemaForNestedRecordWithList(); + TableSchema unsupportedConverterSchema = + new DelegatingTableSchemaWithoutConverterForAttribute<>(supportedSchema); + + String tableName = getConcreteTableName("delete-unsupported-converter-table"); + + DynamoDbTable table = + enhancedClient.table(tableName, unsupportedConverterSchema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + NestedImmutableRecordWithList item = NestedImmutableRecordWithList + .builder() + .id("1") + .time(MOCKED_INSTANT_UPDATE_ONE) + .level2(NestedImmutableChildRecordWithList.builder().time(MOCKED_INSTANT_UPDATE_TWO).build()) + .build(); + + table.putItem(r -> r.item(item)); + + table.deleteItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(table.getItem(r -> r.key(k -> k.partitionValue("1"))), is(nullValue())); + + table.deleteTable(); + } + private WriteModification invokeBeforeWriteForPutItem(Map itemAttributes, TableSchema tableSchema) { return invokeBeforeWriteForPutItem(itemAttributes, tableSchema.tableMetadata(), tableSchema); @@ -2521,4 +2748,67 @@ public boolean isAbstract() { // converterForAttribute is intentionally NOT overridden. } + + private static class DelegatingTableSchemaWithoutConverterForAttribute implements TableSchema { + private final TableSchema delegate; + + private DelegatingTableSchemaWithoutConverterForAttribute(TableSchema delegate) { + this.delegate = delegate; + } + + @Override + public T mapToItem(Map attributeMap) { + return delegate.mapToItem(attributeMap); + } + + @Override + public Map itemToMap(T item, boolean ignoreNulls) { + return delegate.itemToMap(item, ignoreNulls); + } + + @Override + public Map itemToMap(T item, java.util.Collection attributes) { + return delegate.itemToMap(item, attributes); + } + + @Override + public AttributeValue attributeValue(T item, String attributeName) { + return delegate.attributeValue(item, attributeName); + } + + @Override + public TableMetadata tableMetadata() { + return delegate.tableMetadata(); + } + + @Override + public EnhancedType itemType() { + return delegate.itemType(); + } + + @Override + public List attributeNames() { + return delegate.attributeNames(); + } + + @Override + public boolean isAbstract() { + return delegate.isAbstract(); + } + + // converterForAttribute is intentionally NOT overridden, so the TableSchema default implementation + // throws UnsupportedOperationException. + } + + private static final class DelegatingTableSchemaWithNullConverterForAttribute + extends DelegatingTableSchemaWithoutConverterForAttribute { + private DelegatingTableSchemaWithNullConverterForAttribute(TableSchema delegate) { + super(delegate); + } + + @Override + public AttributeConverter converterForAttribute(Object key) { + return null; + } + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java index e59ea214399b..a6c207e1be0e 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java @@ -314,6 +314,28 @@ public void updateItemConditionTestFailure() { } + @Test + public void putOverwrite_withExplicitUuidValues_shouldRegenerateAllUuidsAffectedByBehavior() { + mappedTable.putItem(r -> r.item(new Record().id("overwrite-id").attribute("one"))); + Record first = mappedTable.getItem(r -> r.key(k -> k.partitionValue("overwrite-id"))); + + Record overwrite = new Record().id("overwrite-id") + .attribute("two") + .createdUuid("00000000-0000-0000-0000-000000000000") + .lastUpdatedUuid("00000000-0000-0000-0000-000000000001") + .flattenedRecord(new FlattenedRecord().generated("00000000-0000-0000-0000-000000000002")); + mappedTable.putItem(overwrite); + + Record second = mappedTable.getItem(r -> r.key(k -> k.partitionValue("overwrite-id"))); + assertRecordHasValidUuid(second); + Assertions.assertThat(second.getCreatedUuid()).isNotEqualTo(overwrite.getCreatedUuid()); + Assertions.assertThat(second.getLastUpdatedUuid()).isNotEqualTo(overwrite.getLastUpdatedUuid()); + Assertions.assertThat(second.getFlattenedRecord().getGenerated()) + .isNotEqualTo(overwrite.getFlattenedRecord().getGenerated()); + Assertions.assertThat(second.getCreatedUuid()).isNotEqualTo(first.getCreatedUuid()); + Assertions.assertThat(second.getAttribute()).isEqualTo("two"); + } + public static Record createUniqueFakeItem() { Record record = new Record(); record.setId(UUID.randomUUID().toString()); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicCrudTest.java index 8d59158b2931..84bd0b6e1b6f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicCrudTest.java @@ -17,6 +17,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; @@ -25,6 +26,8 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; import java.util.Objects; +import java.util.HashMap; +import java.util.Map; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -35,12 +38,15 @@ import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ProjectionType; @@ -656,4 +662,29 @@ public void getAShortRecordWithNewModelledFields() { Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); assertThat(result, is(expectedRecord)); } + + @Test + public void putAndUpdate_withUnsupportedCustomSchema_shouldSucceedWithoutException() { + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable> customTable = + extensionClient.table(getConcreteTableName("crud-custom-table"), new UnsupportedConverterTableSchema()); + customTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Map item = new HashMap<>(); + item.put("pk", AttributeValue.builder().s("custom-1").build()); + item.put("nested", + AttributeValue.builder() + .m(java.util.Collections.singletonMap("k", AttributeValue.builder().s("v").build())) + .build()); + + customTable.putItem(item); + customTable.updateItem(item); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))); + assertThat(persisted, is(notNullValue())); + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("crud-custom-table"))); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchGetItemTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchGetItemTest.java index 6eb92c2d4409..1186ea45c683 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchGetItemTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchGetItemTest.java @@ -26,6 +26,9 @@ import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -41,8 +44,44 @@ import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; public class BatchGetItemTest extends LocalDynamoDbSyncTestBase { + @DynamoDbBean + public static class BeanRecord { + private Integer id; + private String value; + + @DynamoDbPartitionKey + public Integer getId() { return id; } + public void setId(Integer id) { this.id = id; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + } + + @DynamoDbImmutable(builder = ImmutableRecord.Builder.class) + public static final class ImmutableRecord { + private final Integer id; + private final String value; + + private ImmutableRecord(Builder builder) { + this.id = builder.id; + this.value = builder.value; + } + + @DynamoDbPartitionKey + public Integer id() { return id; } + public String value() { return value; } + + public static Builder builder() { return new Builder(); } + public static final class Builder { + private Integer id; + private String value; + public Builder id(Integer id) { this.id = id; return this; } + public Builder value(String value) { this.value = value; return this; } + public ImmutableRecord build() { return new ImmutableRecord(this); } + } + } private static class Record1 { private Integer id; private String stringAttr; @@ -147,6 +186,10 @@ public int hashCode() { private final String tableName2 = getConcreteTableName("table-name-2"); private final DynamoDbTable mappedTable1 = enhancedClient.table(tableName1, TABLE_SCHEMA_1); private final DynamoDbTable mappedTable2 = enhancedClient.table(tableName2, TABLE_SCHEMA_2); + private final DynamoDbTable mappedBeanTable = + enhancedClient.table(getConcreteTableName("table-name-bean"), TableSchema.fromBean(BeanRecord.class)); + private final DynamoDbTable mappedImmutableTable = + enhancedClient.table(getConcreteTableName("table-name-immutable"), TableSchema.fromImmutableClass(ImmutableRecord.class)); private static final List RECORDS_1 = IntStream.range(0, 2) @@ -162,6 +205,8 @@ public int hashCode() { public void createTable() { mappedTable1.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); mappedTable2.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + mappedBeanTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + mappedImmutableTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); } @After @@ -172,6 +217,12 @@ public void deleteTable() { getDynamoDbClient().deleteTable(DeleteTableRequest.builder() .tableName(tableName2) .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-bean")) + .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-immutable")) + .build()); } private void insertRecords() { @@ -290,6 +341,72 @@ public void notFoundRecordIgnored_withReturnConsumedCapacity() { }); } + @Test + public void batchGetRecords_withExtension_shouldPreserveAndReturnCorrectData() { + insertRecords(); + + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + + DynamoDbTable extensionMappedTable1 = extensionClient.table(tableName1, TABLE_SCHEMA_1); + DynamoDbTable extensionMappedTable2 = extensionClient.table(tableName2, TABLE_SCHEMA_2); + + BatchGetItemEnhancedRequest request = BatchGetItemEnhancedRequest.builder() + .readBatches( + ReadBatch.builder(Record1.class) + .mappedTableResource(extensionMappedTable1) + .addGetItem(i -> i.key(k -> k.partitionValue(0))) + .build(), + ReadBatch.builder(Record2.class) + .mappedTableResource(extensionMappedTable2) + .addGetItem(i -> i.key(k -> k.partitionValue(0))) + .build(), + ReadBatch.builder(Record2.class) + .mappedTableResource(extensionMappedTable2) + .addGetItem(i -> i.key(k -> k.partitionValue(1))) + .build(), + ReadBatch.builder(Record1.class) + .mappedTableResource(extensionMappedTable1) + .addGetItem(i -> i.key(k -> k.partitionValue(1))) + .build()) + .build(); + + SdkIterable results = extensionClient.batchGetItem(request); + + assertThat(results.stream().count(), is(1L)); + results.iterator().forEachRemaining(page -> { + List table1Results = page.resultsForTable(extensionMappedTable1); + assertThat(table1Results, containsInAnyOrder(RECORDS_1.toArray())); + + List table2Results = page.resultsForTable(extensionMappedTable2); + assertThat(table2Results, containsInAnyOrder(RECORDS_2.toArray())); + }); + } + + @Test + public void batchGetRecords_fromMixedSchemaTypes_shouldReturnCorrectlyCastItems() { + insertRecords(); + BeanRecord beanRecord = new BeanRecord(); + beanRecord.setId(10); + beanRecord.setValue("bean"); + mappedBeanTable.putItem(beanRecord); + ImmutableRecord immutableRecord = ImmutableRecord.builder().id(20).value("immutable").build(); + mappedImmutableTable.putItem(immutableRecord); + + BatchGetResultPageIterable pages = enhancedClient.batchGetItem(r -> r.readBatches( + ReadBatch.builder(Record1.class).mappedTableResource(mappedTable1).addGetItem(i -> i.key(k -> k.partitionValue(0))).build(), + ReadBatch.builder(BeanRecord.class).mappedTableResource(mappedBeanTable).addGetItem(i -> i.key(k -> k.partitionValue(10))).build(), + ReadBatch.builder(ImmutableRecord.class).mappedTableResource(mappedImmutableTable).addGetItem(i -> i.key(k -> k.partitionValue(20))).build())); + + List collected = pages.stream().collect(Collectors.toList()); + assertThat(collected.size(), is(1)); + assertThat(collected.get(0).resultsForTable(mappedTable1), containsInAnyOrder(RECORDS_1.get(0))); + assertThat(collected.get(0).resultsForTable(mappedBeanTable).get(0).getValue(), is("bean")); + assertThat(collected.get(0).resultsForTable(mappedImmutableTable).get(0).value(), is("immutable")); + } + private BatchGetItemEnhancedRequest batchGetItemEnhancedRequestWithNotFoundRecord() { return BatchGetItemEnhancedRequest.builder() .readBatches( diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchWriteItemTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchWriteItemTest.java index ca5bebc33bc4..c9d672349b8d 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchWriteItemTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BatchWriteItemTest.java @@ -17,10 +17,16 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -32,16 +38,42 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteResult; import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics; public class BatchWriteItemTest extends LocalDynamoDbSyncTestBase { + private static class TimestampedRecord { + private String id; + private Instant time; + + private String getId() { + return id; + } + + private TimestampedRecord setId(String id) { + this.id = id; + return this; + } + + private Instant getTime() { + return time; + } + + private TimestampedRecord setTime(Instant time) { + this.time = time; + return this; + } + } + private static class Record1 { private Integer id; private String attribute; @@ -148,12 +180,33 @@ public int hashCode() { .setter(Record2::setAttribute)) .build(); + private static final TableSchema TIMESTAMPED_TABLE_SCHEMA = + StaticTableSchema.builder(TimestampedRecord.class) + .newItemSupplier(TimestampedRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TimestampedRecord::getId) + .setter(TimestampedRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(Instant.class, a -> a.name("time") + .getter(TimestampedRecord::getTime) + .setter(TimestampedRecord::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + private DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) .build(); + private DynamoDbEnhancedClient extensionEnhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); private DynamoDbTable mappedTable1 = enhancedClient.table(getConcreteTableName("table-name-1"), TABLE_SCHEMA_1); private DynamoDbTable mappedTable2 = enhancedClient.table(getConcreteTableName("table-name-2"), TABLE_SCHEMA_2); + private DynamoDbTable timestampedTable = + extensionEnhancedClient.table(getConcreteTableName("timestamped-table"), TIMESTAMPED_TABLE_SCHEMA); + private DynamoDbTable> unsupportedCustomSchemaTable = + extensionEnhancedClient.table(getConcreteTableName("unsupported-schema-table"), new UnsupportedConverterTableSchema()); private static final List RECORDS_1 = IntStream.range(0, 2) @@ -169,6 +222,8 @@ public int hashCode() { public void createTable() { mappedTable1.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); mappedTable2.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + timestampedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + unsupportedCustomSchemaTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); } @After @@ -179,6 +234,12 @@ public void deleteTable() { getDynamoDbClient().deleteTable(DeleteTableRequest.builder() .tableName(getConcreteTableName("table-name-2")) .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("timestamped-table")) + .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("unsupported-schema-table")) + .build()); } @Test @@ -198,6 +259,24 @@ public void singlePut() { assertThat(record, is(RECORDS_1.get(0))); } + @Test + public void putItem_singleWithConsumedCapacity_shouldReturnMetrics() { + BatchWriteResult result = enhancedClient.batchWriteItem(BatchWriteItemEnhancedRequest.builder() + .addWriteBatch( + WriteBatch.builder(Record1.class) + .mappedTableResource(mappedTable1) + .addPutItem(r -> r.item(RECORDS_1.get(0))) + .build()) + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .build()); + + Assertions.assertThat(result.consumedCapacity()).isNotEmpty(); + Assertions.assertThat(result.consumedCapacity().get(0).capacityUnits()).isGreaterThan(0d); + Assertions.assertThat(result.consumedCapacity().get(0).tableName()) + .isEqualTo(getConcreteTableName("table-name-1")); + assertThat(mappedTable1.getItem(r -> r.key(k -> k.partitionValue(0))), is(RECORDS_1.get(0))); + } + @Test public void multiplePut() { BatchWriteItemEnhancedRequest batchWriteItemEnhancedRequest = @@ -273,6 +352,26 @@ public void singleDelete() { assertThat(record, is(nullValue())); } + @Test + public void deleteItem_singleWithConsumedCapacity_shouldReturnMetrics() { + mappedTable1.putItem(r -> r.item(RECORDS_1.get(0))); + + BatchWriteResult result = enhancedClient.batchWriteItem(BatchWriteItemEnhancedRequest.builder() + .addWriteBatch( + WriteBatch.builder(Record1.class) + .mappedTableResource(mappedTable1) + .addDeleteItem(r -> r.key(k -> k.partitionValue(0))) + .build()) + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .build()); + + Assertions.assertThat(result.consumedCapacity()).isNotEmpty(); + Assertions.assertThat(result.consumedCapacity().get(0).capacityUnits()).isGreaterThan(0d); + Assertions.assertThat(result.consumedCapacity().get(0).tableName()) + .isEqualTo(getConcreteTableName("table-name-1")); + assertThat(mappedTable1.getItem(r -> r.key(k -> k.partitionValue(0))), is(nullValue())); + } + @Test public void multipleDelete() { mappedTable1.putItem(r -> r.item(RECORDS_1.get(0))); @@ -384,5 +483,78 @@ public void mixedCommands_withConsumedCapacity() { assertThat(mappedTable2.getItem(r -> r.key(k -> k.partitionValue(0))), is(nullValue())); } + @Test + public void batchWrite_withSupportedAndUnsupportedSchema_shouldPreserveValues() { + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("details", + AttributeValue.builder().m(Collections.singletonMap("inner", + AttributeValue.builder().s("v").build())) + .build()); + + BatchWriteItemEnhancedRequest request = + BatchWriteItemEnhancedRequest.builder() + .writeBatches( + WriteBatch.builder(TimestampedRecord.class) + .mappedTableResource(timestampedTable) + .addPutItem(r -> r.item(new TimestampedRecord().setId("ts-1"))) + .build(), + WriteBatch.builder(customTableType()) + .mappedTableResource(unsupportedCustomSchemaTable) + .addPutItem(r -> r.item(customItem)) + .build()) + .build(); + + extensionEnhancedClient.batchWriteItem(request); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("details").m().get("inner").s(), is("v")); + } + + @Test + public void batchWriteWithResponse_withSupportedAndUnsupportedSchema_shouldReturnMetrics() { + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("details", + AttributeValue.builder().m(Collections.singletonMap("inner", + AttributeValue.builder().s("v").build())) + .build()); + + BatchWriteResult result = + extensionEnhancedClient.batchWriteItem(BatchWriteItemEnhancedRequest.builder() + .writeBatches( + WriteBatch.builder(TimestampedRecord.class) + .mappedTableResource(timestampedTable) + .addPutItem(r -> r.item(new TimestampedRecord().setId("ts-1"))) + .build(), + WriteBatch.builder(customTableType()) + .mappedTableResource(unsupportedCustomSchemaTable) + .addPutItem(r -> r.item(customItem)) + .build()) + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .build()); + + assertThat(result.consumedCapacity(), is(notNullValue())); + assertThat(result.consumedCapacity().size(), is(2)); + assertThat(result.itemCollectionMetrics(), is(notNullValue())); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("details").m().get("inner").s(), is("v")); + } + + @SuppressWarnings("unchecked") + private Class> customTableType() { + return (Class>) (Class) Map.class; + } + } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/DeleteItemWithResponseTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/DeleteItemWithResponseTest.java index 532bf220a842..1d051cae55a2 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/DeleteItemWithResponseTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/DeleteItemWithResponseTest.java @@ -116,4 +116,20 @@ public void returnConsumedCapacity_set_consumedCapacityNotNull() { assertThat(response.consumedCapacity()).isNotNull(); } + + @Test + public void deleteItem_simpleAndWithResponse_shouldReturnSameAttributes() { + Record first = new Record().setId(11).setStringAttr1("a"); + mappedTable1.putItem(first); + Record deletedBySimple = mappedTable1.deleteItem(r -> r.key(k -> k.partitionValue(11))); + assertThat(deletedBySimple).isEqualTo(first); + assertThat(mappedTable1.getItem(r -> r.key(k -> k.partitionValue(11)))).isNull(); + + Record second = new Record().setId(12).setStringAttr1("b"); + mappedTable1.putItem(second); + DeleteItemEnhancedResponse response = + mappedTable1.deleteItemWithResponse(r -> r.key(k -> k.partitionValue(12))); + assertThat(response.attributes()).isEqualTo(second); + assertThat(mappedTable1.getItem(r -> r.key(k -> k.partitionValue(12)))).isNull(); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenMapTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenMapTest.java index ee8f6be5e9bc..625948fb5750 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenMapTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenMapTest.java @@ -20,6 +20,8 @@ import java.util.Collections; import java.util.HashMap; +import java.util.Map; +import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.After; @@ -32,11 +34,15 @@ import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FlattenRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.flattenmap.FlattenMapAndFlattenRecordBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.flattenmap.FlattenMapInvalidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.flattenmap.FlattenMapValidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.flattenmap.NestedFlattenMapBean; +import software.amazon.awssdk.enhanced.dynamodb.model.BatchGetItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.ReadBatch; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; public class FlattenMapTest extends LocalDynamoDbSyncTestBase { @@ -238,4 +244,71 @@ public void updateItemWithNestedFlattenMap_correctlyFlattensMapAttributes() { getDynamoDbClient().deleteTable(r -> r.tableName(nestedTableName)); } } + + @Test + public void updateItem_withUnsupportedCustomSchema_shouldSucceedWithMixedNestedData() { + String customTableName = getConcreteTableName("custom-flatten-table"); + DynamoDbTable> customTable = + enhancedClient.table(customTableName, new UnsupportedConverterTableSchema()); + customTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + try { + Map nested = new HashMap<>(); + nested.put("mapAttribute1", AttributeValue.builder().s("mapValue1").build()); + nested.put("mapAttribute2", AttributeValue.builder().s("mapValue2").build()); + + Map item = new HashMap<>(); + item.put("pk", AttributeValue.builder().s("custom-flat-1").build()); + item.put("flattenLike", AttributeValue.builder().m(nested).build()); + item.put("records", AttributeValue.builder().l( + AttributeValue.builder().m(Collections.singletonMap("k", AttributeValue.builder().s("v").build())).build(), + AttributeValue.builder().s("literal").build()).build()); + + customTable.updateItem(item); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-flat-1"))); + assertThat(persisted.get("flattenLike").m().get("mapAttribute1").s()).isEqualTo("mapValue1"); + assertThat(persisted.get("records").l().get(1).s()).isEqualTo("literal"); + } finally { + getDynamoDbClient().deleteTable(r -> r.tableName(customTableName)); + } + } + + @Test + public void flattenMap_acrossMultipleOperations_shouldPreserveValues() { + FlattenMapValidBean record = new FlattenMapValidBean(); + record.setId("cross-1"); + record.setRootAttribute1("root-1"); + record.setAttributesMap(new HashMap() {{ + put("k1", "v1"); + put("k2", "v2"); + }}); + mappedTable.putItem(record); + + FlattenMapValidBean update = new FlattenMapValidBean(); + update.setId("cross-1"); + update.setRootAttribute2("root-2"); + update.setAttributesMap(new HashMap() {{ + put("k1", "v1u"); + put("k3", "v3u"); + }}); + mappedTable.updateItem(update); + + List scanned = mappedTable.scan().items().stream().collect(Collectors.toList()); + assertThat(scanned.size()).isEqualTo(1); + assertThat(scanned.get(0).getAttributesMap()).containsEntry("k1", "v1u"); + assertThat(scanned.get(0).getAttributesMap()).containsEntry("k3", "v3u"); + + List batchRead = enhancedClient.batchGetItem(BatchGetItemEnhancedRequest.builder() + .readBatches(ReadBatch.builder(FlattenMapValidBean.class) + .mappedTableResource(mappedTable) + .addGetItem(r -> r.key(k -> k.partitionValue("cross-1"))) + .build()) + .build()) + .resultsForTable(mappedTable) + .stream() + .collect(Collectors.toList()); + assertThat(batchRead).hasSize(1); + assertThat(batchRead.get(0).getRootAttribute2()).isEqualTo("root-2"); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenTest.java index 10c343f7dfbb..bd3ece87a228 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenTest.java @@ -20,6 +20,8 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import java.util.Objects; +import java.util.List; +import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -27,6 +29,8 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.BatchGetItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.ReadBatch; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; public class FlattenTest extends LocalDynamoDbSyncTestBase { @@ -202,4 +206,32 @@ public void update_nullDocument() { assertThat(updatedRecord, is(record)); assertThat(fetchedRecord, is(record)); } + + @Test + public void flattenedAttributes_acrossMultipleOperations_shouldPreserveValues() { + Record initial = new Record().setId("id-cross") + .setDocument(new Document().setDocumentAttribute1("a1").setDocumentAttribute2("a2")); + mappedTable.putItem(initial); + + Record update = new Record().setId("id-cross") + .setDocument(new Document().setDocumentAttribute1("a1u").setDocumentAttribute2("a2u") + .setDocumentAttribute3("a3u")); + mappedTable.updateItem(update); + + List scanned = mappedTable.scan().items().stream().collect(Collectors.toList()); + assertThat(scanned.size(), is(1)); + assertThat(scanned.get(0), is(update)); + + List batchRead = enhancedClient.batchGetItem(BatchGetItemEnhancedRequest.builder() + .readBatches(ReadBatch.builder(Record.class) + .mappedTableResource(mappedTable) + .addGetItem(r -> r.key(k -> k.partitionValue("id-cross"))) + .build()) + .build()) + .resultsForTable(mappedTable) + .stream() + .collect(Collectors.toList()); + assertThat(batchRead.size(), is(1)); + assertThat(batchRead.get(0), is(update)); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenWithTagsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenWithTagsTest.java index 9a87f9457b87..c095210b57ef 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenWithTagsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenWithTagsTest.java @@ -21,6 +21,8 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; import java.util.Objects; +import java.util.List; +import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -28,6 +30,8 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.BatchGetItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.ReadBatch; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; public class FlattenWithTagsTest extends LocalDynamoDbSyncTestBase { @@ -192,4 +196,33 @@ public void update_someValues() { assertThat(updatedRecord, is(record)); assertThat(fetchedRecord, is(record)); } + + @Test + public void sortTaggedFlattenedPath_acrossMultipleOperations_shouldPreserveValues() { + Record initial = new Record().setId("id-tag") + .setDocument(new Document().setDocumentAttribute1("s1").setDocumentAttribute2("v2")); + mappedTable.putItem(initial); + + Record update = new Record().setId("id-tag") + .setDocument(new Document().setDocumentAttribute1("s1").setDocumentAttribute2("v2u") + .setDocumentAttribute3("v3u")); + mappedTable.updateItem(update); + + List scanned = mappedTable.scan().items().stream().collect(Collectors.toList()); + assertThat(scanned.size(), is(1)); + assertThat(scanned.get(0), is(update)); + + List batchRead = enhancedClient.batchGetItem(BatchGetItemEnhancedRequest.builder() + .readBatches(ReadBatch.builder(Record.class) + .mappedTableResource(mappedTable) + .addGetItem(r -> r.key(k -> k.partitionValue("id-tag") + .sortValue("s1"))) + .build()) + .build()) + .resultsForTable(mappedTable) + .stream() + .collect(Collectors.toList()); + assertThat(batchRead.size(), is(1)); + assertThat(batchRead.get(0), is(update)); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/GetItemWithResponseTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/GetItemWithResponseTest.java index abce56fe3054..228e1644083b 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/GetItemWithResponseTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/GetItemWithResponseTest.java @@ -31,6 +31,7 @@ import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse; import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; public class GetItemWithResponseTest extends LocalDynamoDbSyncTestBase { @@ -153,6 +154,21 @@ public void getItemWithResponse_withReturnConsumedCapacity_when_recordIsPresent_ assertThat(consumedCapacity.capacityUnits()).isCloseTo(25.0, Offset.offset(0.01)); } + @Test + public void getItem_withExtension_simpleAndWithResponse_shouldReturnSameData() { + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable extensionTable = extensionClient.table(getConcreteTableName(TEST_TABLE_NAME), TABLE_SCHEMA); + Record original = new Record().setId(501).setStringAttr1("abc"); + extensionTable.putItem(original); + + Record getResult = extensionTable.getItem(r -> r.key(k -> k.partitionValue(501))); + GetItemEnhancedResponse response = extensionTable.getItemWithResponse(r -> r.key(k -> k.partitionValue(501))); + assertThat(getResult).isEqualTo(response.attributes()); + } + private static String getStringAttrValue(int numChars) { char[] chars = new char[numChars]; Arrays.fill(chars, 'a'); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactGetItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactGetItemsTest.java index d5a12246dde2..451f933c20b1 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactGetItemsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactGetItemsTest.java @@ -32,6 +32,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.TransactGetItemsEnhancedRequest; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; @@ -186,5 +187,32 @@ public void notFoundRecordReturnsNull() { assertThat(results.get(2).getItem(mappedTable2), is(nullValue())); assertThat(results.get(3).getItem(mappedTable1), is(RECORDS_1.get(1))); } + + @Test + public void transactGetRecords_withExtension_shouldPreserveAndReturnCorrectData() { + insertRecords(); + + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable extensionMappedTable1 = extensionClient.table(getConcreteTableName("table-name-1"), TABLE_SCHEMA_1); + DynamoDbTable extensionMappedTable2 = extensionClient.table(getConcreteTableName("table-name-2"), TABLE_SCHEMA_2); + + TransactGetItemsEnhancedRequest request = + TransactGetItemsEnhancedRequest.builder() + .addGetItem(extensionMappedTable1, Key.builder().partitionValue(0).build()) + .addGetItem(extensionMappedTable2, Key.builder().partitionValue(0).build()) + .addGetItem(extensionMappedTable2, Key.builder().partitionValue(1).build()) + .addGetItem(extensionMappedTable1, Key.builder().partitionValue(1).build()) + .build(); + + List results = extensionClient.transactGetItems(request); + assertThat(results.size(), is(4)); + assertThat(results.get(0).getItem(extensionMappedTable1), is(RECORDS_1.get(0))); + assertThat(results.get(1).getItem(extensionMappedTable2), is(RECORDS_2.get(0))); + assertThat(results.get(2).getItem(extensionMappedTable2), is(RECORDS_2.get(1))); + assertThat(results.get(3).getItem(extensionMappedTable1), is(RECORDS_1.get(1))); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactWriteItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactWriteItemsTest.java index b18e3185c3e9..8eaf5dee5750 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactWriteItemsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/TransactWriteItemsTest.java @@ -21,10 +21,14 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.fail; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import java.time.Instant; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -36,6 +40,8 @@ import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.ConditionCheck; import software.amazon.awssdk.enhanced.dynamodb.model.TransactDeleteItemEnhancedRequest; @@ -48,9 +54,33 @@ import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics; import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; public class TransactWriteItemsTest extends LocalDynamoDbSyncTestBase { + private static class TimestampedRecord { + private String id; + private Instant time; + + private String getId() { + return id; + } + + private TimestampedRecord setId(String id) { + this.id = id; + return this; + } + + private Instant getTime() { + return time; + } + + private TimestampedRecord setTime(Instant time) { + this.time = time; + return this; + } + } + private static class Record1 { private Integer id; private String attribute; @@ -149,12 +179,33 @@ public int hashCode() { .setter(Record2::setAttribute)) .build(); + private static final TableSchema TIMESTAMPED_TABLE_SCHEMA = + StaticTableSchema.builder(TimestampedRecord.class) + .newItemSupplier(TimestampedRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TimestampedRecord::getId) + .setter(TimestampedRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(Instant.class, a -> a.name("time") + .getter(TimestampedRecord::getTime) + .setter(TimestampedRecord::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + private DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) .build(); + private DynamoDbEnhancedClient extensionEnhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); private DynamoDbTable mappedTable1 = enhancedClient.table(getConcreteTableName("table-name-1"), TABLE_SCHEMA_1); private DynamoDbTable mappedTable2 = enhancedClient.table(getConcreteTableName("table-name-2"), TABLE_SCHEMA_2); + private DynamoDbTable timestampedTable = + extensionEnhancedClient.table(getConcreteTableName("timestamped-table"), TIMESTAMPED_TABLE_SCHEMA); + private DynamoDbTable> unsupportedCustomSchemaTable = + extensionEnhancedClient.table(getConcreteTableName("unsupported-schema-table"), new UnsupportedConverterTableSchema()); private static final List RECORDS_1 = IntStream.range(0, 2) @@ -170,6 +221,8 @@ public int hashCode() { public void createTable() { mappedTable1.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); mappedTable2.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + timestampedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + unsupportedCustomSchemaTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); } @After @@ -180,6 +233,12 @@ public void deleteTable() { getDynamoDbClient().deleteTable(DeleteTableRequest.builder() .tableName(getConcreteTableName("table-name-2")) .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("timestamped-table")) + .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("unsupported-schema-table")) + .build()); } @Test @@ -493,5 +552,57 @@ public void mixedCommands_returnValuesOnConditionCheckFailureSet_allConditionsFa cancellationReasons.forEach(r -> assertThat(r.item().isEmpty(), is(false))); } } + + @Test + public void transactWrite_withSupportedAndUnsupportedSchema_shouldPreserveValues() { + TimestampedRecord timestampedRecord = new TimestampedRecord().setId("ts-1"); + + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("payload", + AttributeValue.builder().m(singletonMap("inner", AttributeValue.builder().s("value").build())).build()); + + extensionEnhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(timestampedTable, timestampedRecord) + .addPutItem(unsupportedCustomSchemaTable, customItem) + .build()); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("payload").m().get("inner").s(), is("value")); + } + + @Test + public void transactWriteWithResponse_withSupportedAndUnsupportedSchema_shouldReturnMetrics() { + TimestampedRecord timestampedRecord = new TimestampedRecord().setId("ts-1"); + + Map customItem = new HashMap<>(); + customItem.put("pk", AttributeValue.builder().s("custom-1").build()); + customItem.put("payload", + AttributeValue.builder().m(singletonMap("inner", AttributeValue.builder().s("value").build())).build()); + + TransactWriteItemsEnhancedResponse response = extensionEnhancedClient.transactWriteItemsWithResponse( + TransactWriteItemsEnhancedRequest.builder() + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .addPutItem(timestampedTable, timestampedRecord) + .addPutItem(unsupportedCustomSchemaTable, customItem) + .build()); + + assertThat(response.consumedCapacity(), is(notNullValue())); + assertThat(response.consumedCapacity().size(), is(2)); + assertThat(response.itemCollectionMetrics(), is(notNullValue())); + + TimestampedRecord persistedTimestamped = timestampedTable.getItem(r -> r.key(k -> k.partitionValue("ts-1"))); + Map persistedCustom = + unsupportedCustomSchemaTable.getItem(r -> r.key(k -> k.partitionValue("custom-1"))); + + assertThat(persistedTimestamped.getTime(), is(notNullValue())); + assertThat(persistedCustom.get("payload").m().get("inner").s(), is("value")); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java index fe87b1ece6e0..e6d0bb04bfe3 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java @@ -5,7 +5,9 @@ import static org.junit.Assert.assertTrue; import java.time.Instant; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -14,8 +16,10 @@ import org.junit.Test; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UnsupportedConverterTableSchema; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.CompositeRecord; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FlattenRecord; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordWithUpdateBehavior; @@ -164,6 +168,34 @@ public void updateBehaviors_transactWriteItems_secondUpdate() { assertThat(persistedRecord.getCreatedAutoUpdateOn()).isEqualTo(firstUpdatedRecord.getCreatedAutoUpdateOn()); } + @Test + public void updateItem_withConditionExpression_shouldPreserveWriteIfNotExistsField() { + RecordWithUpdateBehaviors seed = new RecordWithUpdateBehaviors(); + seed.setId("id-cond"); + seed.setCreatedOn(INSTANT_1); + seed.setLastUpdatedOn(INSTANT_1); + mappedTable.putItem(seed); + + RecordWithUpdateBehaviors update = new RecordWithUpdateBehaviors(); + update.setId("id-cond"); + update.setVersion(1L); + update.setCreatedOn(INSTANT_2); + update.setLastUpdatedOn(INSTANT_2); + + mappedTable.updateItem(r -> r.item(update) + .conditionExpression(Expression.builder() + .expression("id = :id") + .expressionValues(Collections.singletonMap(":id", + AttributeValue.builder() + .s("id-cond") + .build())) + .build())); + + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-cond"))); + assertThat(persistedRecord.getCreatedOn()).isEqualTo(INSTANT_1); + assertThat(persistedRecord.getLastUpdatedOn()).isEqualTo(INSTANT_2); + } + @Test public void when_updatingNestedObjectWithSingleLevel_existingInformationIsPreserved_scalar_only_update() { @@ -585,4 +617,96 @@ public void updateBehaviors_nested() { assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNull(); assertThat(persistedRecord.getNestedRecord().getNestedTimeAttribute()).isAfterOrEqualTo(currentTime); } + + @Test + public void updateItem_withUnsupportedSchema_scalarOnlyMode_shouldSucceed() { + DynamoDbTable> customTable = createCustomModeTable(); + + Map item = new java.util.HashMap<>(); + item.put("pk", AttributeValue.builder().s("custom-scalar").build()); + item.put("simple", AttributeValue.builder().s("value").build()); + customTable.updateItem(r -> r.item(item).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-scalar"))); + assertThat(persisted).isNotNull(); + assertThat(persisted.get("simple").s()).isEqualTo("value"); + deleteCustomModeTable(); + } + + @Test + public void updateItem_withUnsupportedSchema_mapsOnlyMode_shouldSucceed() { + DynamoDbTable> customTable = createCustomModeTable(); + + Map item = buildCustomSchemaItem("custom-maps"); + customTable.updateItem(r -> r.item(item).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-maps"))); + assertThat(persisted).isNotNull(); + assertThat(persisted.get("nested").m().get("city").s()).isEqualTo("Seattle"); + deleteCustomModeTable(); + } + + @Test + public void updateItem_withUnsupportedSchema_defaultMode_shouldSucceed() { + DynamoDbTable> customTable = createCustomModeTable(); + + Map item = buildCustomSchemaItem("custom-default"); + customTable.updateItem(item); + + Map persisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-default"))); + assertThat(persisted).isNotNull(); + assertThat(persisted.get("nested").m().get("country").s()).isEqualTo("US"); + deleteCustomModeTable(); + } + + @Test + public void sharedClient_withMixedSchemas_shouldApplySupportedAndSkipUnsupported() { + DynamoDbTable> customTable = createCustomModeTable(); + + RecordWithUpdateBehaviors supportedRecord = new RecordWithUpdateBehaviors(); + supportedRecord.setId("supported-id"); + mappedTable.updateItem(supportedRecord); + + Map customItem = buildCustomSchemaItem("custom-mixed"); + customTable.updateItem(customItem); + + RecordWithUpdateBehaviors supportedPersisted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("supported-id"))); + Map customPersisted = customTable.getItem(r -> r.key(k -> k.partitionValue("custom-mixed"))); + + assertThat(supportedPersisted).isNotNull(); + assertThat(supportedPersisted.getLastAutoUpdatedOn()).isNotNull(); + assertThat(supportedPersisted.getCreatedAutoUpdateOn()).isNotNull(); + assertThat(customPersisted).isNotNull(); + assertThat(customPersisted.get("nested").m().get("city").s()).isEqualTo("Seattle"); + assertThat(customPersisted.get("records").l().get(1).s()).isEqualTo("literal"); + deleteCustomModeTable(); + } + + private DynamoDbTable> createCustomModeTable() { + DynamoDbTable> customTable = + enhancedClient.table(getConcreteTableName("custom-mode-table"), new UnsupportedConverterTableSchema()); + customTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + return customTable; + } + + private void deleteCustomModeTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("custom-mode-table"))); + } + + private Map buildCustomSchemaItem(String key) { + Map nested = new HashMap<>(); + nested.put("city", AttributeValue.builder().s("Seattle").build()); + nested.put("country", AttributeValue.builder().s("US").build()); + Map mapElement = new HashMap<>(); + mapElement.put("inner", AttributeValue.builder().s("value").build()); + + Map item = new HashMap<>(); + item.put("pk", AttributeValue.builder().s(key).build()); + item.put("nested", AttributeValue.builder().m(nested).build()); + item.put("records", AttributeValue.builder().l(Arrays.asList( + AttributeValue.builder().m(mapElement).build(), + AttributeValue.builder().s("literal").build())).build()); + return item; + } + } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateItemWithResponseTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateItemWithResponseTest.java index c7557f7542b8..35daff8c3263 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateItemWithResponseTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateItemWithResponseTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNull; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import java.util.Objects; @@ -29,6 +30,7 @@ import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; public class UpdateItemWithResponseTest extends LocalDynamoDbSyncTestBase { private static class Record { @@ -153,4 +155,56 @@ public void returnItemCollectionMetrics_unset_itemCollectionMetricsNull() { assertThat(response.itemCollectionMetrics()).isNull(); } + + @Test + public void updateItemWithResponse_returnAllOld_shouldMapAttributes() { + Record original = new Record().setId(1).setStringAttr1("attr"); + mappedTable1.putItem(original); + Record updated = new Record().setId(1).setStringAttr1("attr2"); + UpdateItemEnhancedResponse response = mappedTable1.updateItemWithResponse(r -> r.item(updated) + .returnValues(ReturnValue.ALL_OLD)); + assertThat(response.attributes()).isEqualTo(original); + } + + @Test + public void updateItemWithResponse_returnAllNew_shouldMapAttributes() { + Record original = new Record().setId(1).setStringAttr1("attr"); + mappedTable1.putItem(original); + Record updated = new Record().setId(1).setStringAttr1("attr2"); + UpdateItemEnhancedResponse response = mappedTable1.updateItemWithResponse(r -> r.item(updated) + .returnValues(ReturnValue.ALL_NEW)); + assertThat(response.attributes()).isEqualTo(updated); + } + + @Test + public void updateItemWithResponse_returnNone_shouldHaveNullAttributes() { + Record original = new Record().setId(1).setStringAttr1("attr"); + mappedTable1.putItem(original); + Record updated = new Record().setId(1).setStringAttr1("attr2"); + UpdateItemEnhancedResponse response = mappedTable1.updateItemWithResponse(r -> r.item(updated) + .returnValues(ReturnValue.NONE)); + assertNull(response.attributes()); + } + + @Test + public void updateItemWithResponse_returnUpdatedOld_shouldMapAttributes() { + Record original = new Record().setId(1).setStringAttr1("attr"); + mappedTable1.putItem(original); + Record updated = new Record().setId(1).setStringAttr1("attr2"); + UpdateItemEnhancedResponse response = mappedTable1.updateItemWithResponse(r -> r.item(updated) + .returnValues(ReturnValue.UPDATED_OLD)); + assertThat(response.attributes().getStringAttr1()).isEqualTo(original.stringAttr1); + assertThat(response.attributes().getId()).isNull(); + } + + @Test + public void updateItemWithResponse_returnUpdatedNew_shouldMapAttributes() { + Record original = new Record().setId(1).setStringAttr1("attr"); + mappedTable1.putItem(original); + Record updated = new Record().setId(1).setStringAttr1("attr2"); + UpdateItemEnhancedResponse response = mappedTable1.updateItemWithResponse(r -> r.item(updated) + .returnValues(ReturnValue.UPDATED_NEW)); + assertThat(response.attributes().getStringAttr1()).isEqualTo(updated.getStringAttr1()); + assertThat(response.attributes().getId()).isNull(); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java index 9d9eec77cabc..c601ac6e0759 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/VersionedRecordTest.java @@ -41,6 +41,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; @@ -725,4 +726,21 @@ public void annotatedRecord_startAtNegativeOne_firstVersionIsZero() { AnnotatedRecordStartAtNegativeOne result = annotatedStartAtNegativeOneTable.getItem(r -> r.key(k -> k.partitionValue("test-id"))); assertThat(result.getVersion(), is(0L)); } + + @Test + public void transactWriteUpdate_versionedRecord_shouldIncrementVersion() { + mappedTable.putItem(r -> r.item(new Record().setId("tx-id").setAttribute("one"))); + Record existing = mappedTable.getItem(r -> r.key(k -> k.partitionValue("tx-id"))); + + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addUpdateItem(mappedTable, + new Record().setId("tx-id") + .setAttribute("two") + .setVersion(existing.getVersion())) + .build()); + + Record updated = mappedTable.getItem(r -> r.key(k -> k.partitionValue("tx-id"))); + assertThat(updated.getVersion(), is(existing.getVersion() + 1)); + assertThat(updated.getAttribute(), is("two")); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncBasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncBasicQueryTest.java new file mode 100644 index 000000000000..3e352e8d5c03 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncBasicQueryTest.java @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbAsyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.Select; + +public class AsyncBasicQueryTest extends LocalDynamoDbAsyncTestBase { + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build()) + .collect(Collectors.toList()); + + private final String tableName = getConcreteTableName("doc-table-name"); + private DynamoDbEnhancedAsyncClient enhancedClient; + private DynamoDbAsyncTable docMappedTable; + + @Before + public void setup() { + enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build(); + docMappedTable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .build()); + docMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void cleanup() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build()).join(); + } + + private void insertDocuments() { + DOCUMENTS.forEach(d -> docMappedTable.putItem(d).join()); + } + + @Test + public void queryAllRecordsDefaultSettings_shortcutForm() { + insertDocuments(); + SdkPublisher> publisher = docMappedTable.query(keyEqualTo(k -> k.partitionValue("id-value"))); + List> pages = drainPublisher(publisher, 1); + assertThat(pages.get(0).items().size(), is(DOCUMENTS.size())); + } + + @Test + public void queryAllRecords_withProjection_shouldSelectOnlyProjectedAttributes() { + insertDocuments(); + SdkPublisher> publisher = + docMappedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .attributesToProject("value") + .select("SPECIFIC_ATTRIBUTES")); + List> pages = drainPublisher(publisher, 1); + EnhancedDocument first = pages.get(0).items().get(0); + assertThat(first.getString("id"), is(nullValue())); + assertThat(first.getNumber("sort"), is(nullValue())); + assertThat(first.getNumber("value").intValue(), is(0)); + } + + @Test + public void queryAllRecordsDefaultSettings_withSelectCount() { + insertDocuments(); + SdkPublisher> publisher = + docMappedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .select(Select.COUNT)); + Page page = drainPublisher(publisher, 1).get(0); + assertThat(page.count(), is(DOCUMENTS.size())); + assertThat(page.items(), is(empty())); + } + + @Test + public void queryRecords_withDottedAttributeName_shouldProjectCorrectly() { + EnhancedDocument nested = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", 1) + .putString("test.com", "v1") + .build(); + docMappedTable.putItem(nested).join(); + + SdkPublisher> publisher = + docMappedTable.query(b -> b.queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .addNestedAttributeToProject(NestedAttributeName.create("test.com"))); + EnhancedDocument first = drainPublisher(publisher, 1).get(0).items().get(0); + assertThat(first.getString("test.com"), is("v1")); + } + + @Test + public void queryExclusiveStartKey() { + insertDocuments(); + java.util.Map exclusiveStartKey = + new java.util.HashMap<>(); + exclusiveStartKey.put("id", stringValue("id-value")); + exclusiveStartKey.put("sort", numberValue(7)); + + SdkPublisher> publisher = + docMappedTable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .exclusiveStartKey(exclusiveStartKey) + .build()); + List> pages = drainPublisher(publisher, 1); + assertThat(pages.get(0).items().isEmpty(), is(false)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncBasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncBasicScanTest.java new file mode 100644 index 000000000000..36fb6c1556a5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncBasicScanTest.java @@ -0,0 +1,133 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbAsyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.Select; + +public class AsyncBasicScanTest extends LocalDynamoDbAsyncTestBase { + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build()) + .collect(Collectors.toList()); + + private final String tableName = getConcreteTableName("doc-table-name"); + private DynamoDbEnhancedAsyncClient enhancedClient; + private DynamoDbAsyncTable docMappedTable; + + @Before + public void setup() { + enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build(); + docMappedTable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .build()); + docMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void cleanup() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build()).join(); + } + + private void insertDocuments() { + DOCUMENTS.forEach(d -> docMappedTable.putItem(d).join()); + } + + @Test + public void scanAllRecordsDefaultSettings() { + insertDocuments(); + Page page = drainPublisher(docMappedTable.scan(), 1).get(0); + assertThat(page.items().size(), is(DOCUMENTS.size())); + } + + @Test + public void scanAllRecordsWithFilterAndProjection() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + SdkPublisher> publisher = + docMappedTable.scan(ScanEnhancedRequest.builder() + .attributesToProject("sort") + .filterExpression(expression) + .build()); + Page page = drainPublisher(publisher, 1).get(0); + assertThat(page.items().size(), is(3)); + assertThat(page.items().get(0).getString("id"), is(nullValue())); + assertThat(page.items().get(0).getNumber("sort").intValue(), is(3)); + } + + @Test + public void scanAllRecords_withSelectCount_shouldReturnCount() { + insertDocuments(); + Page page = drainPublisher(docMappedTable.scan(b -> b.select(Select.COUNT)), 1).get(0); + assertThat(page.count(), is(DOCUMENTS.size())); + assertThat(page.items(), is(empty())); + } + + @Test + public void scanExclusiveStartKey() { + insertDocuments(); + Map key = new HashMap<>(); + key.put("id", stringValue("id-value")); + key.put("sort", numberValue(7)); + Page page = drainPublisher(docMappedTable.scan(b -> b.exclusiveStartKey(key)), 1).get(0); + assertThat(page.items().size(), is(2)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncIndexQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncIndexQueryTest.java new file mode 100644 index 000000000000..a718cf4293e6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncIndexQueryTest.java @@ -0,0 +1,147 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncIndex; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbAsyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class AsyncIndexQueryTest extends LocalDynamoDbAsyncTestBase { + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putString("gsi_id", "gsi-id-value") + .putNumber("gsi_sort", i) + .build()) + .collect(Collectors.toList()); + + private final String tableName = getConcreteTableName("doc-table-name"); + private DynamoDbEnhancedAsyncClient enhancedClient; + private DynamoDbAsyncTable table; + private DynamoDbAsyncIndex index; + + @Before + public void setup() { + enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build(); + table = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.N) + .addIndexPartitionKey("gsi_keys_only", "gsi_id", AttributeValueType.S) + .addIndexSortKey("gsi_keys_only", "gsi_sort", AttributeValueType.N) + .build()); + table.createTable(CreateTableEnhancedRequest.builder() + .provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_keys_only") + .projection(p -> p.projectionType(ProjectionType.KEYS_ONLY)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build()) + .build()).join(); + index = table.index("gsi_keys_only"); + } + + @After + public void cleanup() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build()).join(); + } + + private void insertDocuments() { + DOCUMENTS.forEach(d -> table.putItem(d).join()); + } + + @Test + public void queryAllRecordsDefaultSettings_usingShortcutForm() { + insertDocuments(); + Page page = drainPublisher(index.query(keyEqualTo(k -> k.partitionValue("gsi-id-value"))), 1).get(0); + assertThat(page.items().size(), is(DOCUMENTS.size())); + } + + @Test + public void queryBetween() { + insertDocuments(); + Key fromKey = Key.builder().partitionValue("gsi-id-value").sortValue(3).build(); + Key toKey = Key.builder().partitionValue("gsi-id-value").sortValue(5).build(); + SdkPublisher> publisher = + index.query(r -> r.queryConditional(QueryConditional.sortBetween(fromKey, toKey))); + Page page = drainPublisher(publisher, 1).get(0); + assertThat(page.items().size(), is(3)); + } + + @Test + public void queryLimit() { + insertDocuments(); + List> pages = drainPublisher(index.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value"))) + .limit(5) + .build()), 3); + assertThat(pages.get(0).items().size(), is(5)); + assertThat(pages.get(2).items(), is(empty())); + } + + @Test + public void queryExclusiveStartKey() { + insertDocuments(); + Map startKey = new HashMap<>(); + startKey.put("id", stringValue("id-value")); + startKey.put("sort", numberValue(7)); + startKey.put("gsi_id", stringValue("gsi-id-value")); + startKey.put("gsi_sort", numberValue(7)); + Page page = drainPublisher(index.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value"))) + .exclusiveStartKey(startKey) + .build()), 1).get(0); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + assertThat(page.items().size(), is(2)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncIndexScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncIndexScanTest.java new file mode 100644 index 000000000000..c0c05699626e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/AsyncIndexScanTest.java @@ -0,0 +1,143 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncIndex; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbAsyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class AsyncIndexScanTest extends LocalDynamoDbAsyncTestBase { + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putString("gsi_id", "gsi-id-value") + .putNumber("gsi_sort", i) + .build()) + .collect(Collectors.toList()); + + private final String tableName = getConcreteTableName("doc-table-name"); + private DynamoDbEnhancedAsyncClient enhancedClient; + private DynamoDbAsyncTable table; + private DynamoDbAsyncIndex index; + + @Before + public void setup() { + enhancedClient = DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build(); + table = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.N) + .addIndexPartitionKey("gsi_keys_only", "gsi_id", AttributeValueType.S) + .addIndexSortKey("gsi_keys_only", "gsi_sort", AttributeValueType.N) + .build()); + table.createTable(CreateTableEnhancedRequest.builder() + .provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_keys_only") + .projection(p -> p.projectionType(ProjectionType.KEYS_ONLY)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build()) + .build()).join(); + index = table.index("gsi_keys_only"); + } + + @After + public void cleanup() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build()).join(); + } + + private void insertDocuments() { + DOCUMENTS.forEach(d -> table.putItem(d).join()); + } + + @Test + public void scanAllRecordsDefaultSettings() { + insertDocuments(); + Page page = drainPublisher(index.scan(), 1).get(0); + assertThat(page.items().size(), is(DOCUMENTS.size())); + } + + @Test + public void scanAllRecordsWithFilter() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("sort >= :min_value AND sort <= :max_value") + .expressionValues(expressionValues) + .build(); + Page page = drainPublisher(index.scan(ScanEnhancedRequest.builder() + .filterExpression(expression) + .build()), 1).get(0); + assertThat(page.items().size(), is(3)); + } + + @Test + public void scanLimit() { + insertDocuments(); + List> pages = drainPublisher(index.scan(r -> r.limit(5)), 3); + assertThat(pages.get(0).items().size(), is(5)); + assertThat(pages.get(2).items(), is(empty())); + } + + @Test + public void scanExclusiveStartKey() { + insertDocuments(); + Map startKey = new HashMap<>(); + startKey.put("id", stringValue("id-value")); + startKey.put("sort", numberValue(7)); + startKey.put("gsi_id", stringValue("gsi-id-value")); + startKey.put("gsi_sort", numberValue(7)); + Page page = drainPublisher(index.scan(r -> r.exclusiveStartKey(startKey)), 1).get(0); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + assertThat(page.items().size(), is(2)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java index 74d445228e5e..4222c08a2bc6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java @@ -18,10 +18,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.CompletionException; @@ -44,15 +46,23 @@ import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbAsyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteResult; import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedResponse; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics; @RunWith(Parameterized.class) public class BasicAsyncCrudTest extends LocalDynamoDbAsyncTestBase { @@ -593,4 +603,191 @@ public void updateWithConditionThatFails() { .conditionExpression(conditionExpression) .build()).join(); } + + @Test + public void documentSchema_withExtension_putAndUpdate_shouldSucceed() { + String extensionTableName = getConcreteTableName("doc-extension-table"); + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + EnhancedDocument document = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("payload", "value") + .build(); + extensionTable.putItem(document).join(); + extensionTable.updateItem(document).join(); + + EnhancedDocument persisted = extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))).join(); + assertThat(persisted, is(notNullValue())); + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(extensionTableName)).join(); + } + + @Test + public void documentSchema_withExtension_shouldPreserveNonTimestampValues() { + String extensionTableName = getConcreteTableName("doc-extension-integrity-table"); + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + Map nested = new LinkedHashMap<>(); + nested.put("time", AttributeValue.fromS("nested-original")); + nested.put("flag", AttributeValue.fromS("x")); + + Map itemMap = new LinkedHashMap<>(); + itemMap.put("id", AttributeValue.fromS("doc-1")); + itemMap.put("time", AttributeValue.fromS("root-original")); + itemMap.put("payload", AttributeValue.fromM(nested)); + itemMap.put("records", AttributeValue.fromL(Collections.singletonList(AttributeValue.fromM(nested)))); + + EnhancedDocument document = EnhancedDocument.fromAttributeValueMap(itemMap).toBuilder() + .attributeConverterProviders(defaultProvider()) + .build(); + + extensionTable.putItem(document).join(); + extensionTable.updateItem(document).join(); + + EnhancedDocument persisted = extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))).join(); + assertThat(persisted.toMap().get("time").s(), is("root-original")); + assertThat(persisted.toMap().get("payload").m().get("time").s(), is("nested-original")); + assertThat(persisted.toMap().get("records").l().get(0).m().get("time").s(), is("nested-original")); + + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(extensionTableName)).join(); + } + + @Test + public void documentSchema_withExtension_deleteItem_shouldSucceed() { + String extensionTableName = getConcreteTableName("doc-extension-delete-table"); + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + EnhancedDocument document = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("payload", "value") + .build(); + extensionTable.putItem(document).join(); + extensionTable.deleteItem(r -> r.key(k -> k.partitionValue("doc-1"))).join(); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))).join(), is(nullValue())); + + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(extensionTableName)).join(); + } + + @Test + public void documentSchemaBatchWrite_withExtension_shouldPreserveValuesAndReturnMetrics() { + String extensionTableName = getConcreteTableName("doc-extension-batch-table"); + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + EnhancedDocument first = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("time", "value-one") + .build(); + EnhancedDocument second = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-2") + .putString("time", "value-two") + .build(); + + BatchWriteResult result = + extensionClient.batchWriteItem(BatchWriteItemEnhancedRequest.builder() + .writeBatches( + WriteBatch.builder(EnhancedDocument.class) + .mappedTableResource(extensionTable) + .addPutItem(r -> r.item(first)) + .addPutItem(r -> r.item(second)) + .build()) + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .build()).join(); + + assertThat(result.consumedCapacity(), is(notNullValue())); + assertThat(result.itemCollectionMetrics(), is(notNullValue())); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))).join().toMap().get("time").s(), + is("value-one")); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-2"))).join().toMap().get("time").s(), + is("value-two")); + + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(extensionTableName)).join(); + } + + @Test + public void documentSchemaTransactWrite_withExtension_shouldPreserveValuesAndReturnMetrics() { + String extensionTableName = getConcreteTableName("doc-extension-transact-table"); + DynamoDbEnhancedAsyncClient extensionClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbAsyncTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + EnhancedDocument first = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("time", "value-one") + .build(); + EnhancedDocument second = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-2") + .putString("time", "value-two") + .build(); + + TransactWriteItemsEnhancedResponse response = + extensionClient.transactWriteItemsWithResponse(TransactWriteItemsEnhancedRequest.builder() + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .addPutItem(extensionTable, first) + .addPutItem(extensionTable, second) + .build()).join(); + + assertThat(response.consumedCapacity(), is(notNullValue())); + assertThat(response.itemCollectionMetrics(), is(notNullValue())); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))).join().toMap().get("time").s(), + is("value-one")); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-2"))).join().toMap().get("time").s(), + is("value-two")); + + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(extensionTableName)).join(); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java index e4068d2ce292..f4b157519dc6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java @@ -17,11 +17,13 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.assertj.core.api.Assertions; @@ -42,15 +44,23 @@ import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteResult; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedResponse; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics; @RunWith(Parameterized.class) @@ -577,4 +587,186 @@ public void updateWithConditionThatFails() { .conditionExpression(conditionExpression) .build()); } + + @Test + public void documentSchema_withExtension_putAndUpdate_shouldSucceed() { + String extensionTableName = getConcreteTableName("doc-extension-table"); + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + EnhancedDocument document = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("payload", "value") + .build(); + extensionTable.putItem(document); + extensionTable.updateItem(document); + + EnhancedDocument persisted = extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))); + assertThat(persisted, is(notNullValue())); + getDynamoDbClient().deleteTable(r -> r.tableName(extensionTableName)); + } + + @Test + public void documentSchema_withExtension_shouldPreserveNonTimestampValues() { + String extensionTableName = getConcreteTableName("doc-extension-integrity-table"); + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Map nested = new LinkedHashMap<>(); + nested.put("time", AttributeValue.fromS("nested-original")); + nested.put("flag", AttributeValue.fromS("x")); + + Map itemMap = new LinkedHashMap<>(); + itemMap.put("id", AttributeValue.fromS("doc-1")); + itemMap.put("time", AttributeValue.fromS("root-original")); + itemMap.put("payload", AttributeValue.fromM(nested)); + itemMap.put("records", AttributeValue.fromL(Collections.singletonList(AttributeValue.fromM(nested)))); + + EnhancedDocument document = EnhancedDocument.fromAttributeValueMap(itemMap).toBuilder() + .attributeConverterProviders(defaultProvider()) + .build(); + + extensionTable.putItem(document); + extensionTable.updateItem(document); + + EnhancedDocument persisted = extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))); + assertThat(persisted.toMap().get("time").s(), is("root-original")); + assertThat(persisted.toMap().get("payload").m().get("time").s(), is("nested-original")); + assertThat(persisted.toMap().get("records").l().get(0).m().get("time").s(), is("nested-original")); + + getDynamoDbClient().deleteTable(r -> r.tableName(extensionTableName)); + } + + @Test + public void documentSchema_withExtension_deleteItem_shouldSucceed() { + String extensionTableName = getConcreteTableName("doc-extension-delete-table"); + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + EnhancedDocument document = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("payload", "value") + .build(); + extensionTable.putItem(document); + extensionTable.deleteItem(r -> r.key(k -> k.partitionValue("doc-1"))); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))), is(nullValue())); + + getDynamoDbClient().deleteTable(r -> r.tableName(extensionTableName)); + } + + @Test + public void documentSchemaBatchWrite_withExtension_shouldPreserveValuesAndReturnMetrics() { + String extensionTableName = getConcreteTableName("doc-extension-batch-table"); + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + EnhancedDocument first = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("time", "value-one") + .build(); + EnhancedDocument second = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-2") + .putString("time", "value-two") + .build(); + + BatchWriteResult result = extensionClient.batchWriteItem(BatchWriteItemEnhancedRequest.builder() + .writeBatches( + WriteBatch.builder(EnhancedDocument.class) + .mappedTableResource(extensionTable) + .addPutItem(r -> r.item(first)) + .addPutItem(r -> r.item(second)) + .build()) + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .build()); + + assertThat(result.consumedCapacity(), is(notNullValue())); + assertThat(result.itemCollectionMetrics(), is(notNullValue())); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))).toMap().get("time").s(), is("value-one")); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-2"))).toMap().get("time").s(), is("value-two")); + + getDynamoDbClient().deleteTable(r -> r.tableName(extensionTableName)); + } + + @Test + public void documentSchemaTransactWrite_withExtension_shouldPreserveValuesAndReturnMetrics() { + String extensionTableName = getConcreteTableName("doc-extension-transact-table"); + DynamoDbEnhancedClient extensionClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.create()) + .build(); + DynamoDbTable extensionTable = + extensionClient.table(extensionTableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + extensionTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + EnhancedDocument first = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-1") + .putString("time", "value-one") + .build(); + EnhancedDocument second = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "doc-2") + .putString("time", "value-two") + .build(); + + TransactWriteItemsEnhancedResponse response = + extensionClient.transactWriteItemsWithResponse(TransactWriteItemsEnhancedRequest.builder() + .returnConsumedCapacity(ReturnConsumedCapacity.TOTAL) + .returnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE) + .addPutItem(extensionTable, first) + .addPutItem(extensionTable, second) + .build()); + + assertThat(response.consumedCapacity(), is(notNullValue())); + assertThat(response.itemCollectionMetrics(), is(notNullValue())); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-1"))).toMap().get("time").s(), is("value-one")); + assertThat(extensionTable.getItem(r -> r.key(k -> k.partitionValue("doc-2"))).toMap().get("time").s(), is("value-two")); + + getDynamoDbClient().deleteTable(r -> r.tableName(extensionTableName)); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UnsupportedConverterTableSchema.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UnsupportedConverterTableSchema.java new file mode 100644 index 000000000000..d1d2fc7e3dae --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UnsupportedConverterTableSchema.java @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Pass-through map schema that intentionally keeps the default {@link TableSchema#converterForAttribute(Object)} + * implementation, which throws {@link UnsupportedOperationException}. + */ +@SuppressWarnings("unchecked") +public final class UnsupportedConverterTableSchema implements TableSchema> { + private final TableMetadata tableMetadata = StaticTableMetadata.builder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "pk", + AttributeValueType.S) + .build(); + + @Override + public Map mapToItem(Map attributeMap) { + return attributeMap == null ? Collections.emptyMap() : new HashMap<>(attributeMap); + } + + @Override + public Map itemToMap(Map item, boolean ignoreNulls) { + if (item == null) { + return Collections.emptyMap(); + } + + if (!ignoreNulls) { + return new HashMap<>(item); + } + + return item.entrySet() + .stream() + .filter(e -> e.getValue() != null && !Boolean.TRUE.equals(e.getValue().nul())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public Map itemToMap(Map item, Collection attributes) { + if (item == null || attributes == null) { + return Collections.emptyMap(); + } + + return attributes.stream() + .filter(item::containsKey) + .collect(Collectors.toMap(a -> a, item::get)); + } + + @Override + public AttributeValue attributeValue(Map item, String attributeName) { + return item == null ? null : item.get(attributeName); + } + + @Override + public TableMetadata tableMetadata() { + return tableMetadata; + } + + @Override + public EnhancedType> itemType() { + return (EnhancedType>) (EnhancedType) EnhancedType.of(Map.class); + } + + @Override + public List attributeNames() { + return Collections.singletonList("pk"); + } + + @Override + public boolean isAbstract() { + return false; + } +}