Skip to content

Commit 105c225

Browse files
committed
Fix UnsupportedOperationException in auto gen timestamps for custom TableSchema
1 parent a7974d0 commit 105c225

6 files changed

Lines changed: 172 additions & 3 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"description": "Fix AutoGeneratedTimestampRecordExtension failing with UnsupportedOperationException for custom table schemas that do not implement converterForAttribute."
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ public static boolean hasMap(AttributeValue attributeValue) {
219219
*
220220
* @param parentSchema the parent schema; must not be null
221221
* @param attributeName the attribute name; must not be null or empty
222-
* @return the nested schema, or empty if unavailable
222+
* @return the nested schema, or empty if unavailable (including when the schema does not support
223+
* {@code converterForAttribute})
223224
*/
224225
public static Optional<TableSchema<?>> getNestedSchema(TableSchema<?> parentSchema, String attributeName) {
225226
if (parentSchema == null) {
@@ -229,7 +230,15 @@ public static Optional<TableSchema<?>> getNestedSchema(TableSchema<?> parentSche
229230
throw new IllegalArgumentException("Attribute name cannot be null or empty.");
230231
}
231232

232-
AttributeConverter<?> converter = parentSchema.converterForAttribute(attributeName);
233+
AttributeConverter<?> converter;
234+
try {
235+
converter = parentSchema.converterForAttribute(attributeName);
236+
} catch (UnsupportedOperationException e) {
237+
// TableSchema implementations that do not support converterForAttribute (e.g. DocumentTableSchema,
238+
// third-party or custom schema implementations) throw UnsupportedOperationException.
239+
// Treat this the same as a missing converter: no nested schema is available.
240+
return Optional.empty();
241+
}
233242
if (converter == null) {
234243
return Optional.empty();
235244
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,15 @@ public static TableSchema<?> getTableSchemaForListElement(
8484
return staticSchema.get();
8585
}
8686

87-
AttributeConverter<?> converter = rootSchema.converterForAttribute(key);
87+
AttributeConverter<?> converter;
88+
try {
89+
converter = rootSchema.converterForAttribute(key);
90+
} catch (UnsupportedOperationException e) {
91+
// TableSchema implementations that do not support converterForAttribute (e.g. DocumentTableSchema,
92+
// third-party or custom schema implementations) throw UnsupportedOperationException.
93+
// Return null to indicate no schema introspection is possible.
94+
return null;
95+
}
8896
if (converter == null) {
8997
throw new IllegalArgumentException("No converter found for attribute: " + key);
9098
}

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,77 @@ public void beforeWrite_mapWithCustomConverter_whenValueTypeNotAnnotated_thenTim
14931493
assertThat(transformed.get("customMap"), is(originalMap));
14941494
}
14951495

1496+
@Test
1497+
public void beforeWrite_documentTableSchemaWithMapAttribute_doesNotThrow() {
1498+
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema docSchema =
1499+
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema.builder()
1500+
.addIndexPartitionKey("primary", "pk",
1501+
software.amazon.awssdk.enhanced.dynamodb.AttributeValueType.S)
1502+
.build();
1503+
1504+
Map<String, AttributeValue> innerMap = new HashMap<>();
1505+
innerMap.put("nestedKey", AttributeValue.builder().s("nestedValue").build());
1506+
1507+
Map<String, AttributeValue> itemMap = new HashMap<>();
1508+
itemMap.put("pk", AttributeValue.builder().s("doc-1").build());
1509+
itemMap.put("data", AttributeValue.builder().m(innerMap).build());
1510+
1511+
WriteModification modification = invokeBeforeWriteForPutItem(itemMap, docSchema.tableMetadata(), docSchema);
1512+
1513+
// DocumentTableSchema has no timestamp metadata, so no transformation should occur
1514+
assertThat(modification.transformedItem(), is(nullValue()));
1515+
}
1516+
1517+
@Test
1518+
public void beforeWrite_documentTableSchemaWithListOfMapsAttribute_doesNotThrow() {
1519+
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema docSchema =
1520+
software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema.builder()
1521+
.addIndexPartitionKey("primary", "pk",
1522+
software.amazon.awssdk.enhanced.dynamodb.AttributeValueType.S)
1523+
.build();
1524+
1525+
Map<String, AttributeValue> innerMap = new HashMap<>();
1526+
innerMap.put("field1", AttributeValue.builder().s("value1").build());
1527+
1528+
Map<String, AttributeValue> itemMap = new HashMap<>();
1529+
itemMap.put("pk", AttributeValue.builder().s("doc-2").build());
1530+
itemMap.put("records", AttributeValue.builder()
1531+
.l(Arrays.asList(AttributeValue.builder().m(innerMap).build()))
1532+
.build());
1533+
1534+
WriteModification modification = invokeBeforeWriteForPutItem(itemMap, docSchema.tableMetadata(), docSchema);
1535+
1536+
assertThat(modification.transformedItem(), is(nullValue()));
1537+
}
1538+
1539+
@Test
1540+
public void beforeWrite_customTableSchemaWithoutConverterForAttribute_doesNotThrow() {
1541+
// Simulates third-party or custom TableSchema implementations
1542+
// that implement TableSchema directly without overriding converterForAttribute.
1543+
TableSchema<?> customSchema = new MinimalTableSchema();
1544+
1545+
Map<String, AttributeValue> locationMap = new HashMap<>();
1546+
locationMap.put("city", AttributeValue.builder().s("Seattle").build());
1547+
locationMap.put("country", AttributeValue.builder().s("US").build());
1548+
1549+
Map<String, AttributeValue> metadataMap = new HashMap<>();
1550+
metadataMap.put("key1", AttributeValue.builder().s("val1").build());
1551+
1552+
Map<String, AttributeValue> itemMap = new HashMap<>();
1553+
itemMap.put("pk", AttributeValue.builder().s("person123#req456").build());
1554+
itemMap.put("location", AttributeValue.builder().m(locationMap).build());
1555+
itemMap.put("metadata", AttributeValue.builder().m(metadataMap).build());
1556+
itemMap.put("records", AttributeValue.builder()
1557+
.l(Arrays.asList(AttributeValue.builder().m(locationMap).build()))
1558+
.build());
1559+
1560+
WriteModification modification = invokeBeforeWriteForPutItem(itemMap,
1561+
customSchema.tableMetadata(),
1562+
customSchema);
1563+
1564+
assertThat(modification.transformedItem(), is(nullValue()));
1565+
}
1566+
14961567
private <T> WriteModification invokeBeforeWriteForPutItem(Map<String, AttributeValue> itemAttributes,
14971568
TableSchema<T> tableSchema) {
14981569
return invokeBeforeWriteForPutItem(itemAttributes, tableSchema.tableMetadata(), tableSchema);
@@ -2393,4 +2464,61 @@ public void setNested(NestedWithUnknownMetadataTimestampKey nested) {
23932464
this.nested = nested;
23942465
}
23952466
}
2467+
2468+
/**
2469+
* A minimal {@link TableSchema} implementation that does NOT override {@code converterForAttribute}.
2470+
* This simulates third-party or custom {@code TableSchema} implementations
2471+
* that implement the interface directly without overriding the default method (which throws
2472+
* {@code UnsupportedOperationException}).
2473+
*/
2474+
@SuppressWarnings("unchecked")
2475+
private static class MinimalTableSchema implements TableSchema<Map<String, Object>> {
2476+
private final TableMetadata tableMetadata = software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata
2477+
.builder()
2478+
.addIndexPartitionKey("primary", "pk",
2479+
software.amazon.awssdk.enhanced.dynamodb.AttributeValueType.S)
2480+
.build();
2481+
2482+
@Override
2483+
public Map<String, Object> mapToItem(Map<String, AttributeValue> attributeMap) {
2484+
return new HashMap<>();
2485+
}
2486+
2487+
@Override
2488+
public Map<String, AttributeValue> itemToMap(Map<String, Object> item, boolean ignoreNulls) {
2489+
return new HashMap<>();
2490+
}
2491+
2492+
@Override
2493+
public Map<String, AttributeValue> itemToMap(Map<String, Object> item, java.util.Collection<String> attributes) {
2494+
return new HashMap<>();
2495+
}
2496+
2497+
@Override
2498+
public AttributeValue attributeValue(Map<String, Object> item, String attributeName) {
2499+
return null;
2500+
}
2501+
2502+
@Override
2503+
public TableMetadata tableMetadata() {
2504+
return tableMetadata;
2505+
}
2506+
2507+
@Override
2508+
public EnhancedType<Map<String, Object>> itemType() {
2509+
return (EnhancedType<Map<String, Object>>) (EnhancedType<?>) EnhancedType.of(Map.class);
2510+
}
2511+
2512+
@Override
2513+
public java.util.List<String> attributeNames() {
2514+
return Collections.singletonList("pk");
2515+
}
2516+
2517+
@Override
2518+
public boolean isAbstract() {
2519+
return false;
2520+
}
2521+
2522+
// converterForAttribute is intentionally NOT overridden.
2523+
}
23962524
}

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ public void getNestedSchema_withNullConverter_returnsEmpty() {
135135
assertThat(result).isEmpty();
136136
}
137137

138+
@Test
139+
public void getNestedSchema_whenConverterForAttributeThrowsUnsupportedOperationException_returnsEmpty() {
140+
when(mockSchema.converterForAttribute("anyAttribute")).thenThrow(new UnsupportedOperationException());
141+
142+
Optional<TableSchema<?>> result = EnhancedClientUtils.getNestedSchema(mockSchema, "anyAttribute");
143+
144+
assertThat(result).isEmpty();
145+
}
146+
138147
@Test
139148
public void getNestedSchema_withNullParentSchema_throwsIllegalArgumentException() {
140149
assertThatThrownBy(() -> EnhancedClientUtils.getNestedSchema(null, "attributeName"))

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ public void getTableSchemaForListElement_withNullConverter_throwsIllegalArgument
127127
.hasMessageContaining("No converter found for attribute: nonExistentAttribute");
128128
}
129129

130+
@Test
131+
public void getTableSchemaForListElement_whenConverterForAttributeThrowsUnsupportedOperationException_returnsNull() {
132+
when(mockSchema.converterForAttribute("anyAttribute")).thenThrow(new UnsupportedOperationException());
133+
134+
TableSchema<?> result = NestedRecordUtils.getTableSchemaForListElement(mockSchema, "anyAttribute");
135+
136+
assertThat(result).isNull();
137+
}
138+
130139
@Test
131140
public void getTableSchemaForListElement_withEmptyRawClassParameters_throwsIllegalArgumentException() {
132141
when(mockSchema.converterForAttribute("emptyParamsAttribute")).thenReturn(mockAttributeConverter);

0 commit comments

Comments
 (0)