This is an automated email from the ASF dual-hosted git repository.
Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new b240fd35dc0 Encode DynamoDB empty list and empty map attributes
(#39731)
b240fd35dc0 is described below
commit b240fd35dc0b25128842ed4d590c9ff00a84e4f5
Author: ZIHAN DAI <[email protected]>
AuthorDate: Wed Sep 16 00:10:47 2026 +1000
Encode DynamoDB empty list and empty map attributes (#39731)
AttributeValueCoder.encode selected the L and M branches with a size
check. DynamoDB allows an empty L and an empty M, so both guards fail for
one,
nul() is null, and it reaches the terminal else and throws
CoderException("Unknown Type"). One empty list anywhere inside an item
makes the whole item unencodable, since MAP_ATTRIBUTE_CODER re-enters
this coder for every child.
hasL()/hasM() are the SDK's way of separating "unset" from "set but
empty", which is exactly the distinction the size check lost.
The ss/ns/bs guards look identical and are deliberately left alone:
DynamoDB rejects empty sets, so an empty one there is not a value worth
preserving.
The decode side already handles both -- case l: and case m: delegate to
ListCoder/MapCoder, which round-trip empties.
---
.../sdk/io/aws2/dynamodb/AttributeValueCoder.java | 7 ++-
.../io/aws2/dynamodb/AttributeValueCoderTest.java | 65 ++++++++++++++++++++++
2 files changed, 70 insertions(+), 2 deletions(-)
diff --git
a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoder.java
b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoder.java
index 79d480e5ef0..2fa57e0c5f8 100644
---
a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoder.java
+++
b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoder.java
@@ -89,10 +89,13 @@ public class AttributeValueCoder extends
AtomicCoder<AttributeValue> {
} else if (value.bs() != null && value.bs().size() > 0) {
StringUtf8Coder.of().encode(AttributeValueType.bs.toString(), outStream);
LIST_BYTE_CODER.encode(convertToListByteArray(value.bs()), outStream);
- } else if (value.l() != null && value.l().size() > 0) {
+ } else if (value.hasL()) {
+ // hasL/hasM rather than a size check: DynamoDB allows an empty L or M,
and the size guard
+ // sent those to the terminal else. The ss/ns/bs guards below keep their
size check on
+ // purpose -- DynamoDB rejects empty sets, so an empty one there is not
a value to preserve.
StringUtf8Coder.of().encode(AttributeValueType.l.toString(), outStream);
LIST_ATTRIBUTE_CODER.encode(value.l(), outStream);
- } else if (value.m() != null && value.m().size() > 0) {
+ } else if (value.hasM()) {
StringUtf8Coder.of().encode(AttributeValueType.m.toString(), outStream);
MAP_ATTRIBUTE_CODER.encode(value.m(), outStream);
} else if (value.nul() != null) {
diff --git
a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoderTest.java
b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoderTest.java
index 89a76baf440..02a95388f73 100644
---
a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoderTest.java
+++
b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/dynamodb/AttributeValueCoderTest.java
@@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.beam.sdk.coders.CoderException;
import
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
import org.junit.Assert;
import org.junit.Test;
@@ -205,4 +206,68 @@ public class AttributeValueCoderTest {
Assert.assertEquals(expected, actual);
}
+
+ @Test
+ public void shouldPassForEmptyListType() throws IOException {
+ AttributeValue expected = AttributeValue.builder().l(new
ArrayList<>()).build();
+
+ AttributeValue actual = roundTrip(expected);
+
+ Assert.assertEquals(expected, actual);
+ Assert.assertTrue("an empty L must decode back as a set-but-empty list",
actual.hasL());
+ }
+
+ @Test
+ public void shouldPassForEmptyMapType() throws IOException {
+ AttributeValue expected = AttributeValue.builder().m(new
HashMap<>()).build();
+
+ AttributeValue actual = roundTrip(expected);
+
+ Assert.assertEquals(expected, actual);
+ Assert.assertTrue("an empty M must decode back as a set-but-empty map",
actual.hasM());
+ }
+
+ @Test
+ public void shouldPassForEmptyListNestedInMap() throws IOException {
+ // The realistic trigger: one empty list anywhere inside an item makes the
whole item
+ // unencodable, not just that attribute.
+ Map<String, AttributeValue> item = new HashMap<>();
+ item.put("name", AttributeValue.builder().s("widget").build());
+ item.put("tags", AttributeValue.builder().l(new ArrayList<>()).build());
+ AttributeValue expected = AttributeValue.builder().m(item).build();
+
+ AttributeValue actual = roundTrip(expected);
+
+ Assert.assertEquals(expected, actual);
+ Assert.assertTrue(actual.m().get("tags").hasL());
+ }
+
+ @Test
+ public void shouldPassForEmptyMapNestedInList() throws IOException {
+ AttributeValue expected =
+ AttributeValue.builder()
+ .l(ImmutableList.of(AttributeValue.builder().m(new
HashMap<>()).build()))
+ .build();
+
+ AttributeValue actual = roundTrip(expected);
+
+ Assert.assertEquals(expected, actual);
+ Assert.assertTrue(actual.l().get(0).hasM());
+ }
+
+ @Test
+ public void shouldStillRejectAnAttributeValueWithNoTypeSet() throws
IOException {
+ // Control: the terminal else must keep rejecting a genuinely typeless
value, so the fix does
+ // not turn "Unknown Type" into silently encoding nothing.
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Assert.assertThrows(
+ CoderException.class,
+ () ->
AttributeValueCoder.of().encode(AttributeValue.builder().build(), out));
+ }
+
+ private static AttributeValue roundTrip(AttributeValue value) throws
IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ AttributeValueCoder.of().encode(value, out);
+ return AttributeValueCoder.of().decode(new
ByteArrayInputStream(out.toByteArray()));
+ }
}