Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "Amazon DynamoDB Enhanced Client",
"contributor": "",
"description": "Fix AutoGeneratedTimestampRecordExtension failing when TableSchema.converterForAttribute throws UnsupportedOperationException for custom schemas; expand dynamodb-enhanced functional test coverage"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -390,7 +397,13 @@ private Map<String, AttributeValue> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.extensions;

import static org.assertj.core.api.Assertions.assertThat;
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

public class ExtensionSpiContractTest {

private static final String TABLE_NAME = "extension-spi-contract-table";

private static final TableSchema<NoExtensionItem> TABLE_SCHEMA = StaticTableSchema.builder(NoExtensionItem.class)
.newItemSupplier(NoExtensionItem::new)
.addAttribute(String.class,
a -> a.name("id")
.getter(NoExtensionItem::getId)
.setter(NoExtensionItem::setId)
.tags(primaryPartitionKey()))
.build();

private static final TableMetadata TABLE_METADATA = TABLE_SCHEMA.tableMetadata();

private static final List<OperationName> WRITE_OPERATIONS = Arrays.asList(
OperationName.PUT_ITEM,
OperationName.UPDATE_ITEM,
OperationName.BATCH_WRITE_ITEM,
OperationName.TRANSACT_WRITE_ITEMS
);

private static Stream<Arguments> extensionAndOperations() {
List<Supplier<DynamoDbEnhancedClientExtension>> extensionSuppliers = Arrays.asList(
AutoGeneratedTimestampRecordExtension::create,
AutoGeneratedUuidExtension::create,
() -> AtomicCounterExtension.builder().build(),
() -> VersionedRecordExtension.builder().build()
);

return extensionSuppliers.stream()
.flatMap(supplier -> WRITE_OPERATIONS.stream()
.map(operation -> Arguments.of(supplier.get(),
operation)));
}

@ParameterizedTest(name = "{0} with {1}")
@MethodSource("extensionAndOperations")
public void beforeWrite_noOptionalCapabilities_returnsNoOpAndDoesNotMutateItems(
DynamoDbEnhancedClientExtension extension,
OperationName operationName) {

Map<String, AttributeValue> items = new HashMap<>();
items.put("id", AttributeValue.builder().s("id1").build());

Map<String, AttributeValue> originalItems = new HashMap<>(items);

DefaultDynamoDbExtensionContext context = DefaultDynamoDbExtensionContext.builder()
.items(items)
.tableMetadata(TABLE_METADATA)
.tableSchema(TABLE_SCHEMA)
.operationName(operationName)
.operationContext(DefaultOperationContext.create(TABLE_NAME))
.build();

WriteModification modification = extension.beforeWrite(context);

assertThat(items).isEqualTo(originalItems);
assertThat(modification).isEqualTo(WriteModification.builder().build());
}

private static final class NoExtensionItem {
private String id;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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;

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<AtomicCounterRecord> TABLE_SCHEMA = TableSchema.fromClass(AtomicCounterRecord.class);

private final DynamoDbEnhancedAsyncClient enhancedAsyncClient =
DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build();

private final DynamoDbAsyncTable<AtomicCounterRecord> 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();
// AtomicCounterRecord annotations: defaultCounter (delta=1, start=0), customCounter (delta=5, start=10),
// decreasingCounter (delta=-1, start=-20). First updateItem creates the item using start values;
// each subsequent update adds delta. After 3 updateItem calls: 0+1+1=2, 10+5+5=20, -20-1-1=-22.
assertThat(persisted.getDefaultCounter()).isEqualTo(2L);
assertThat(persisted.getCustomCounter()).isEqualTo(20L);
assertThat(persisted.getDecreasingCounter()).isEqualTo(-22L);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;

import java.time.Clock;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
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.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithCustomConvertedList;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.CustomConvertedPojo;
import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient;
import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch;
import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteItemEnhancedRequest;
import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;

public class AsyncAutoGeneratedTimestampConvertedByListTest extends LocalDynamoDbAsyncTestBase {
private static final Instant MOCKED_INSTANT_NOW = Instant.parse("2019-01-13T14:00:00Z");

private DynamoDbEnhancedAsyncClient extensionEnhancedAsyncClient;
private DynamoDbAsyncTable<BeanWithCustomConvertedList> table;
private String tableName;

private final TableSchema<BeanWithCustomConvertedList> tableSchema =
BeanTableSchema.create(BeanWithCustomConvertedList.class);

private Clock mockClock;

@Before
public void setUp() {
mockClock = Mockito.mock(Clock.class);
Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW);

extensionEnhancedAsyncClient =
DefaultDynamoDbEnhancedAsyncClient.builder()
.dynamoDbClient(getDynamoDbAsyncClient())
.extensions(AutoGeneratedTimestampRecordExtension.builder()
.baseClock(mockClock)
.build())
.build();

tableName = getConcreteTableName("converted-by-list-extension-async-test-table");
table = extensionEnhancedAsyncClient.table(tableName, tableSchema);
table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join();
}

@After
public void tearDown() {
getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder()
.tableName(tableName)
.build()).join();
}

@Test
public void putThenUpdateItem_customConvertedByList_doesNotThrow_setsTimestampAndPreservesCustomItems() {
BeanWithCustomConvertedList initial = new BeanWithCustomConvertedList()
.setId("id1")
.setCustomItems(Arrays.asList(
new CustomConvertedPojo("a", 1),
new CustomConvertedPojo("b", 2)));

table.putItem(r -> r.item(initial)).join();

List<CustomConvertedPojo> updatedCustomItems = Arrays.asList(
new CustomConvertedPojo("c", 3),
new CustomConvertedPojo("d", 4));

BeanWithCustomConvertedList updated = new BeanWithCustomConvertedList()
.setId("id1")
.setCustomItems(updatedCustomItems);

table.updateItem(r -> r.item(updated)).join();

BeanWithCustomConvertedList result =
table.getItem(r -> r.key(k -> k.partitionValue("id1"))).join();

assertThat(result, is(notNullValue()));
assertThat(result.getTime(), is(MOCKED_INSTANT_NOW));
assertCustomItems(result.getCustomItems(), updatedCustomItems);
}

@Test
public void batchWriteItem_customConvertedByList_doesNotThrow_setsTimestampAndPreservesCustomItems() {
BeanWithCustomConvertedList record = new BeanWithCustomConvertedList()
.setId("id2")
.setCustomItems(Arrays.asList(
new CustomConvertedPojo("a", 1),
new CustomConvertedPojo("b", 2)));

WriteBatch writeBatch = WriteBatch.builder(BeanWithCustomConvertedList.class)
.mappedTableResource(table)
.addPutItem(r -> r.item(record))
.build();

extensionEnhancedAsyncClient.batchWriteItem(
BatchWriteItemEnhancedRequest.builder()
.writeBatches(Arrays.asList(writeBatch))
.build()).join();

BeanWithCustomConvertedList result =
table.getItem(r -> r.key(k -> k.partitionValue("id2"))).join();

assertThat(result, is(notNullValue()));
assertThat(result.getTime(), is(MOCKED_INSTANT_NOW));
assertCustomItems(result.getCustomItems(), record.getCustomItems());
}

@Test
public void transactWriteItems_customConvertedByList_doesNotThrow_setsTimestampAndPreservesCustomItems() {
BeanWithCustomConvertedList record = new BeanWithCustomConvertedList()
.setId("id3")
.setCustomItems(Arrays.asList(
new CustomConvertedPojo("a", 1),
new CustomConvertedPojo("b", 2)));

extensionEnhancedAsyncClient.transactWriteItems(
TransactWriteItemsEnhancedRequest.builder()
.addPutItem(table, record)
.build()).join();

BeanWithCustomConvertedList result =
table.getItem(r -> r.key(k -> k.partitionValue("id3"))).join();

assertThat(result, is(notNullValue()));
assertThat(result.getTime(), is(MOCKED_INSTANT_NOW));
assertCustomItems(result.getCustomItems(), record.getCustomItems());
}

private static void assertCustomItems(List<CustomConvertedPojo> actual,
List<CustomConvertedPojo> expected) {
assertThat(actual.size(), is(expected.size()));
for (int i = 0; i < expected.size(); i++) {
assertThat(actual.get(i).getLabel(), is(expected.get(i).getLabel()));
assertThat(actual.get(i).getCount(), is(expected.get(i).getCount()));
}
}
}

Loading
Loading