Skip to content
Merged
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 with UnsupportedOperationException for custom table schemas that do not implement converterForAttribute."
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ public static boolean hasMap(AttributeValue attributeValue) {
*
* @param parentSchema the parent schema; must not be null
* @param attributeName the attribute name; must not be null or empty
* @return the nested schema, or empty if unavailable
* @return the nested schema, or empty if unavailable (including when the schema does not support
* {@code converterForAttribute})
*/
public static Optional<TableSchema<?>> getNestedSchema(TableSchema<?> parentSchema, String attributeName) {
if (parentSchema == null) {
Expand All @@ -229,7 +230,15 @@ public static Optional<TableSchema<?>> getNestedSchema(TableSchema<?> parentSche
throw new IllegalArgumentException("Attribute name cannot be null or empty.");
}

AttributeConverter<?> converter = parentSchema.converterForAttribute(attributeName);
AttributeConverter<?> converter;
try {
converter = parentSchema.converterForAttribute(attributeName);
} catch (UnsupportedOperationException e) {
// TableSchema implementations that do not support converterForAttribute (e.g. DocumentTableSchema,
// third-party or custom schema implementations) throw UnsupportedOperationException.
// Treat this the same as a missing converter: no nested schema is available.
return Optional.empty();
}
if (converter == null) {
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,15 @@ public static TableSchema<?> getTableSchemaForListElement(
return staticSchema.get();
}

AttributeConverter<?> converter = rootSchema.converterForAttribute(key);
AttributeConverter<?> converter;
try {
converter = rootSchema.converterForAttribute(key);
} catch (UnsupportedOperationException e) {
// TableSchema implementations that do not support converterForAttribute (e.g. DocumentTableSchema,
// third-party or custom schema implementations) throw UnsupportedOperationException.
// Return null to indicate no schema introspection is possible.
return null;
}
if (converter == null) {
throw new IllegalArgumentException("No converter found for attribute: " + key);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,77 @@ public void beforeWrite_mapWithCustomConverter_whenValueTypeNotAnnotated_thenTim
assertThat(transformed.get("customMap"), is(originalMap));
}

@Test
public void beforeWrite_documentTableSchemaWithMapAttribute_doesNotThrow() {
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema docSchema =
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema.builder()
.addIndexPartitionKey("primary", "pk",
software.amazon.awssdk.enhanced.dynamodb.AttributeValueType.S)
.build();

Map<String, AttributeValue> innerMap = new HashMap<>();
innerMap.put("nestedKey", AttributeValue.builder().s("nestedValue").build());

Map<String, AttributeValue> itemMap = new HashMap<>();
itemMap.put("pk", AttributeValue.builder().s("doc-1").build());
itemMap.put("data", AttributeValue.builder().m(innerMap).build());

WriteModification modification = invokeBeforeWriteForPutItem(itemMap, docSchema.tableMetadata(), docSchema);

// DocumentTableSchema has no timestamp metadata, so no transformation should occur
assertThat(modification.transformedItem(), is(nullValue()));
}

@Test
public void beforeWrite_documentTableSchemaWithListOfMapsAttribute_doesNotThrow() {
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema docSchema =
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema.builder()
.addIndexPartitionKey("primary", "pk",
software.amazon.awssdk.enhanced.dynamodb.AttributeValueType.S)
.build();

Map<String, AttributeValue> innerMap = new HashMap<>();
innerMap.put("field1", AttributeValue.builder().s("value1").build());

Map<String, AttributeValue> itemMap = new HashMap<>();
itemMap.put("pk", AttributeValue.builder().s("doc-2").build());
itemMap.put("records", AttributeValue.builder()
.l(Arrays.asList(AttributeValue.builder().m(innerMap).build()))
.build());

WriteModification modification = invokeBeforeWriteForPutItem(itemMap, docSchema.tableMetadata(), docSchema);

assertThat(modification.transformedItem(), is(nullValue()));
}

@Test
public void beforeWrite_customTableSchemaWithoutConverterForAttribute_doesNotThrow() {
// Simulates third-party or custom TableSchema implementations
// that implement TableSchema directly without overriding converterForAttribute.
TableSchema<?> customSchema = new MinimalTableSchema();

Map<String, AttributeValue> locationMap = new HashMap<>();
locationMap.put("city", AttributeValue.builder().s("Seattle").build());
locationMap.put("country", AttributeValue.builder().s("US").build());

Map<String, AttributeValue> metadataMap = new HashMap<>();
metadataMap.put("key1", AttributeValue.builder().s("val1").build());

Map<String, AttributeValue> itemMap = new HashMap<>();
itemMap.put("pk", AttributeValue.builder().s("person123#req456").build());
itemMap.put("location", AttributeValue.builder().m(locationMap).build());
itemMap.put("metadata", AttributeValue.builder().m(metadataMap).build());
itemMap.put("records", AttributeValue.builder()
.l(Arrays.asList(AttributeValue.builder().m(locationMap).build()))
.build());

WriteModification modification = invokeBeforeWriteForPutItem(itemMap,
customSchema.tableMetadata(),
customSchema);

assertThat(modification.transformedItem(), is(nullValue()));
}

private <T> WriteModification invokeBeforeWriteForPutItem(Map<String, AttributeValue> itemAttributes,
TableSchema<T> tableSchema) {
return invokeBeforeWriteForPutItem(itemAttributes, tableSchema.tableMetadata(), tableSchema);
Expand Down Expand Up @@ -2393,4 +2464,61 @@ public void setNested(NestedWithUnknownMetadataTimestampKey nested) {
this.nested = nested;
}
}

/**
* A minimal {@link TableSchema} implementation that does NOT override {@code converterForAttribute}.
* This simulates third-party or custom {@code TableSchema} implementations
* that implement the interface directly without overriding the default method (which throws
* {@code UnsupportedOperationException}).
*/
@SuppressWarnings("unchecked")
private static class MinimalTableSchema implements TableSchema<Map<String, Object>> {
private final TableMetadata tableMetadata = software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata
.builder()
.addIndexPartitionKey("primary", "pk",
software.amazon.awssdk.enhanced.dynamodb.AttributeValueType.S)
.build();

@Override
public Map<String, Object> mapToItem(Map<String, AttributeValue> attributeMap) {
return new HashMap<>();
}

@Override
public Map<String, AttributeValue> itemToMap(Map<String, Object> item, boolean ignoreNulls) {
return new HashMap<>();
}

@Override
public Map<String, AttributeValue> itemToMap(Map<String, Object> item, java.util.Collection<String> attributes) {
return new HashMap<>();
}

@Override
public AttributeValue attributeValue(Map<String, Object> item, String attributeName) {
return null;
}

@Override
public TableMetadata tableMetadata() {
return tableMetadata;
}

@Override
public EnhancedType<Map<String, Object>> itemType() {
return (EnhancedType<Map<String, Object>>) (EnhancedType<?>) EnhancedType.of(Map.class);
}

@Override
public java.util.List<String> attributeNames() {
return Collections.singletonList("pk");
}

@Override
public boolean isAbstract() {
return false;
}

// converterForAttribute is intentionally NOT overridden.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ public void getNestedSchema_withNullConverter_returnsEmpty() {
assertThat(result).isEmpty();
}

@Test
public void getNestedSchema_whenConverterForAttributeThrowsUnsupportedOperationException_returnsEmpty() {
when(mockSchema.converterForAttribute("anyAttribute")).thenThrow(new UnsupportedOperationException());

Optional<TableSchema<?>> result = EnhancedClientUtils.getNestedSchema(mockSchema, "anyAttribute");

assertThat(result).isEmpty();
}

@Test
public void getNestedSchema_withNullParentSchema_throwsIllegalArgumentException() {
assertThatThrownBy(() -> EnhancedClientUtils.getNestedSchema(null, "attributeName"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ public void getTableSchemaForListElement_withNullConverter_throwsIllegalArgument
.hasMessageContaining("No converter found for attribute: nonExistentAttribute");
}

@Test
public void getTableSchemaForListElement_whenConverterForAttributeThrowsUnsupportedOperationException_returnsNull() {
when(mockSchema.converterForAttribute("anyAttribute")).thenThrow(new UnsupportedOperationException());

TableSchema<?> result = NestedRecordUtils.getTableSchemaForListElement(mockSchema, "anyAttribute");

assertThat(result).isNull();
}

@Test
public void getTableSchemaForListElement_withEmptyRawClassParameters_throwsIllegalArgumentException() {
when(mockSchema.converterForAttribute("emptyParamsAttribute")).thenReturn(mockAttributeConverter);
Expand Down
Loading