This is an automated email from the ASF dual-hosted git repository.

ahmedabu98 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 2fdcfb830db Render ARRAY and ITERABLE fields that are not backed by a 
List (#39750)
2fdcfb830db is described below

commit 2fdcfb830db716bce9a6af5dd694ef57681e6ac7
Author: ZIHAN DAI <[email protected]>
AuthorDate: Wed Oct 21 05:01:22 2026 +1100

    Render ARRAY and ITERABLE fields that are not backed by a List (#39750)
    
    * Render ARRAY and ITERABLE fields that are not backed by a List
    
    Row#toString throws for an ITERABLE field holding a plain Iterable:
    
      Schema s = Schema.builder().addStringField("k")
          .addIterableField("vals", FieldType.STRING).build();
      Row.withSchema(s).attachValues("k1", () -> list.iterator()).toString();
    
      IllegalArgumentException: value type is '...' for field type 'ITERABLE'
    
    toPrettyFieldValueString demanded a List before iterating. An ITERABLE
    field declares an Iterable, so the guard is stricter than the type it
    guards, and the branch only ever iterates and counts -- both of which
    Iterable supports once materialised.
    
    Losing a whole Row's rendering is a poor trade for a stricter check
    here, since this is what Row#toString calls: one unusual field takes out
    logging and debugger output for every field beside it.
    
    The guard now requires Iterable and materialises once for the size
    count. A value that is neither still throws the same exception.
    
    Two tests: a bare Iterable on an ITERABLE field, which fails on master,
    and an ordinary List array as a control that passes either way. The
    first also asserts the value really is not a List before rendering, so
    it cannot go green for the wrong reason if the value ever starts
    arriving materialised.
    
    * Render the collection in one pass instead of copying it
    
    Review feedback from @ahmedabu98: keep the original iterable rather than
    recreating it as a list.
    
    The copy was load-bearing as the block stood, because it asked the
    collection for isEmpty(), then size(), then iterated it -- three
    traversals. A List absorbs that for free; an Iterable that can only be
    read once does not, and that is exactly the kind of value this PR widens
    support to.
    
    Doing it in a single pass gets both: no copy, and no assumption that the
    collection can be traversed more than once. Output is unchanged --
    elements are separated by ",\n" as before, and an empty collection still
    renders as "[]".
    
    Two tests added. The one-shot case hands out its iterator exactly once
    and throws on a second attempt; with Iterables.isEmpty plus
    Iterables.size over the raw iterable it fails with "iterated more than
    once", and it is the only one of the thirteen that does.
    
    * Remove comment
---
 .../org/apache/beam/sdk/schemas/SchemaUtils.java   | 32 ++++++-----
 .../apache/beam/sdk/schemas/SchemaUtilsTest.java   | 67 ++++++++++++++++++++++
 2 files changed, 84 insertions(+), 15 deletions(-)

diff --git 
a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java 
b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java
index 88809182efc..d45454be90f 100644
--- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java
+++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java
@@ -18,7 +18,6 @@
 package org.apache.beam.sdk.schemas;
 
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import org.apache.beam.sdk.schemas.Schema.FieldType;
@@ -270,7 +269,11 @@ public class SchemaUtils {
       case ARRAY:
       case ITERABLE:
         {
-          if (!(value instanceof List)) {
+          // An ITERABLE field declares an Iterable, and an ARRAY value need 
not be a List
+          // either, so require only what is actually iterated below. This is 
reached from
+          // Row#toString, where refusing to render one field takes out 
logging and debugger
+          // output for the entire row.
+          if (!(value instanceof Iterable)) {
             throw new IllegalArgumentException(
                 String.format(
                     "value type is '%s' for field type '%s'",
@@ -279,24 +282,23 @@ public class SchemaUtils {
           FieldType elementType = 
Objects.requireNonNull(fieldType.getCollectionElementType());
 
           @SuppressWarnings("unchecked")
-          List<Object> list = (List<Object>) value;
-          if (list.isEmpty()) {
-            return "[]";
-          }
+          Iterable<Object> elements = (Iterable<Object>) value;
+
           StringBuilder sb = new StringBuilder();
           sb.append("[\n");
-          int size = list.size();
-          int index = 0;
-          for (Object element : list) {
-            sb.append(nextPrefix)
-                .append(toPrettyFieldValueString(elementType, element, 
nextPrefix));
-            if (index++ < size - 1) {
+          boolean empty = true;
+          for (Object element : elements) {
+            if (!empty) {
               sb.append(",\n");
-            } else {
-              sb.append("\n");
             }
+            sb.append(nextPrefix)
+                .append(toPrettyFieldValueString(elementType, element, 
nextPrefix));
+            empty = false;
+          }
+          if (empty) {
+            return "[]";
           }
-          sb.append(prefix).append("]");
+          sb.append("\n").append(prefix).append("]");
           return sb.toString();
         }
       case MAP:
diff --git 
a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java 
b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java
index c34832f58fb..6a5312fcc48 100644
--- 
a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java
+++ 
b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java
@@ -21,7 +21,9 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import org.apache.beam.sdk.schemas.Schema.FieldType;
 import org.apache.beam.sdk.values.Row;
@@ -147,4 +149,69 @@ public class SchemaUtilsTest {
     assertTrue(rendered, rendered.contains("1"));
     assertTrue(rendered, rendered.contains("null"));
   }
+
+  @Test
+  public void testToPrettyStringRendersIterableThatIsNotAList() {
+    Schema schema =
+        Schema.builder().addStringField("k").addIterableField("vals", 
FieldType.STRING).build();
+    // A bare Iterable, which is exactly what an ITERABLE field declares.
+    Iterable<String> bare = () -> Arrays.asList("p", "q").iterator();
+    Row row = Row.withSchema(schema).attachValues("k1", bare);
+
+    // The precondition that makes this test meaningful: if the value ever 
starts arriving as a
+    // List, the assertion below would pass for the wrong reason.
+    assertTrue(!(row.getValue("vals") instanceof java.util.List));
+
+    String rendered = row.toString();
+    assertTrue(rendered, rendered.contains("p"));
+    assertTrue(rendered, rendered.contains("q"));
+  }
+
+  @Test
+  public void testToPrettyStringStillRendersAListArray() {
+    // Control: the ordinary List case must be unchanged.
+    Schema schema = Schema.builder().addArrayField("vals", 
FieldType.STRING).build();
+    Row row = Row.withSchema(schema).attachValues((Object) Arrays.asList("p", 
"q"));
+
+    String rendered = row.toString();
+    assertTrue(rendered, rendered.contains("p"));
+    assertTrue(rendered, rendered.contains("q"));
+  }
+
+  @Test
+  public void testToPrettyStringRendersAnIterableThatCanOnlyBeReadOnce() {
+    Schema schema =
+        Schema.builder().addStringField("k").addIterableField("vals", 
FieldType.STRING).build();
+    // Hands out its iterator exactly once and fails loudly on a second 
attempt. Rendering asks
+    // the collection for one traversal now; asking for isEmpty(), then 
size(), then iterating
+    // would trip the guard below.
+    Iterable<String> onceOnly =
+        new Iterable<String>() {
+          private boolean taken = false;
+
+          @Override
+          public Iterator<String> iterator() {
+            if (taken) {
+              throw new IllegalStateException("iterated more than once");
+            }
+            taken = true;
+            return Arrays.asList("p", "q").iterator();
+          }
+        };
+    Row row = Row.withSchema(schema).attachValues("k1", onceOnly);
+
+    String rendered = row.toString();
+    assertTrue(rendered, rendered.contains("p"));
+    assertTrue(rendered, rendered.contains("q"));
+  }
+
+  @Test
+  public void testToPrettyStringRendersAnEmptyIterableAsEmptyBrackets() {
+    Schema schema = Schema.builder().addIterableField("vals", 
FieldType.STRING).build();
+    Iterable<String> empty = Collections::emptyIterator;
+    Row row = Row.withSchema(schema).attachValues((Object) empty);
+
+    String rendered = row.toString();
+    assertTrue(rendered, rendered.contains("[]"));
+  }
 }

Reply via email to