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

claudevdm 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 2abc3fb11fc Improve GBK coder validation errors (#39800)
2abc3fb11fc is described below

commit 2abc3fb11fcd18e8659018199a8e755d2b507b98
Author: Goutam Adwant <[email protected]>
AuthorDate: Fri Sep 4 08:47:48 2026 -0700

    Improve GBK coder validation errors (#39800)
---
 sdks/python/apache_beam/runners/pipeline_utils.py  | 30 +++++--
 .../apache_beam/runners/pipeline_utils_test.py     | 93 ++++++++++++++++++++++
 2 files changed, 115 insertions(+), 8 deletions(-)

diff --git a/sdks/python/apache_beam/runners/pipeline_utils.py 
b/sdks/python/apache_beam/runners/pipeline_utils.py
index c0c5e199c7d..69ea91cdb6d 100644
--- a/sdks/python/apache_beam/runners/pipeline_utils.py
+++ b/sdks/python/apache_beam/runners/pipeline_utils.py
@@ -93,16 +93,30 @@ def validate_pipeline_graph(pipeline_proto):
       if output_coder.spec.urn != common_urns.coders.KV.urn:
         raise ValueError(
             "Bad coder for output of %s: %s" % (transform_id, output_coder))
+      input_key_coder_id = input_coder.component_coder_ids[0]
+      output_key_coder_id = output_coder.component_coder_ids[0]
+      if input_key_coder_id != output_key_coder_id:
+        raise ValueError(
+            "Input key coder %s does not match output key coder %s for "
+            "transform %s" %
+            (input_key_coder_id, output_key_coder_id, transform_id))
+      output_values_coder_id = output_coder.component_coder_ids[1]
       output_values_coder = pipeline_proto.components.coders[
-          output_coder.component_coder_ids[1]]
-      if (input_coder.component_coder_ids[0]
-          != output_coder.component_coder_ids[0] or
-          output_values_coder.spec.urn != common_urns.coders.ITERABLE.urn or
-          output_values_coder.component_coder_ids[0]
-          != input_coder.component_coder_ids[1]):
+          output_values_coder_id]
+      if output_values_coder.spec.urn != common_urns.coders.ITERABLE.urn:
+        raise ValueError(
+            "Output value coder %s for transform %s must be an iterable "
+            "coder, but uses URN %s" % (
+                output_values_coder_id,
+                transform_id,
+                output_values_coder.spec.urn))
+      input_value_coder_id = input_coder.component_coder_ids[1]
+      output_value_coder_id = output_values_coder.component_coder_ids[0]
+      if output_value_coder_id != input_value_coder_id:
         raise ValueError(
-            "Incompatible input coder %s and output coder %s for transform %s" 
%
-            (transform_id, input_coder, output_coder))
+            "Input value coder %s does not match output value coder %s for "
+            "transform %s" %
+            (input_value_coder_id, output_value_coder_id, transform_id))
     elif transform_proto.spec.urn == common_urns.primitives.ASSIGN_WINDOWS.urn:
       if not transform_proto.inputs:
         raise ValueError("Missing input for transform: %s" % transform_proto)
diff --git a/sdks/python/apache_beam/runners/pipeline_utils_test.py 
b/sdks/python/apache_beam/runners/pipeline_utils_test.py
index ba144f5e6cc..b0669a9e694 100644
--- a/sdks/python/apache_beam/runners/pipeline_utils_test.py
+++ b/sdks/python/apache_beam/runners/pipeline_utils_test.py
@@ -28,10 +28,103 @@ from apache_beam.portability import common_urns
 from apache_beam.portability.api import beam_runner_api_pb2
 from apache_beam.runners.pipeline_utils import merge_common_environments
 from apache_beam.runners.pipeline_utils import merge_superset_dep_environments
+from apache_beam.runners.pipeline_utils import validate_pipeline_graph
 from apache_beam.runners.portability.expansion_service_test import FibTransform
 
 
 class PipelineUtilitiesTest(unittest.TestCase):
+  @staticmethod
+  def _pipeline_with_gbk_coders(
+      input_key_coder='key_coder',
+      output_key_coder='key_coder',
+      output_values_coder_urn=common_urns.coders.ITERABLE.urn,
+      input_value_coder='input_value_coder',
+      output_value_coder='input_value_coder'):
+    leaf_coders = {
+        coder_id: beam_runner_api_pb2.Coder(
+            spec=beam_runner_api_pb2.FunctionSpec(
+                urn=common_urns.coders.BYTES.urn))
+        for coder_id in {
+            input_key_coder,
+            output_key_coder,
+            input_value_coder,
+            output_value_coder, }
+    }
+    output_values_coder = beam_runner_api_pb2.Coder(
+        spec=beam_runner_api_pb2.FunctionSpec(urn=output_values_coder_urn),
+        component_coder_ids=([output_value_coder] if output_values_coder_urn
+                             == common_urns.coders.ITERABLE.urn else []))
+    return beam_runner_api_pb2.Pipeline(
+        components=beam_runner_api_pb2.Components(
+            coders={
+                **leaf_coders,
+                'input_coder': beam_runner_api_pb2.Coder(
+                    spec=beam_runner_api_pb2.FunctionSpec(
+                        urn=common_urns.coders.KV.urn),
+                    component_coder_ids=[input_key_coder, input_value_coder]),
+                'output_coder': beam_runner_api_pb2.Coder(
+                    spec=beam_runner_api_pb2.FunctionSpec(
+                        urn=common_urns.coders.KV.urn),
+                    component_coder_ids=[
+                        output_key_coder, 'output_values_coder'
+                    ]),
+                'output_values_coder': output_values_coder,
+            },
+            pcollections={
+                'input': beam_runner_api_pb2.PCollection(
+                    coder_id='input_coder'),
+                'output': beam_runner_api_pb2.PCollection(
+                    coder_id='output_coder'),
+            },
+            transforms={
+                'gbk': beam_runner_api_pb2.PTransform(
+                    spec=beam_runner_api_pb2.FunctionSpec(
+                        urn=common_urns.primitives.GROUP_BY_KEY.urn),
+                    inputs={'input': 'input'},
+                    outputs={'output': 'output'}),
+            }),
+        root_transform_ids=['gbk'])
+
+  def test_validate_pipeline_graph_accepts_valid_gbk_coders(self):
+    validate_pipeline_graph(self._pipeline_with_gbk_coders())
+
+  def test_validate_pipeline_graph_reports_gbk_key_coder_mismatch(self):
+    pipeline = self._pipeline_with_gbk_coders(
+        input_key_coder='input_key_coder', output_key_coder='output_key_coder')
+
+    with self.assertRaises(ValueError) as error:
+      validate_pipeline_graph(pipeline)
+
+    self.assertEqual(
+        str(error.exception),
+        'Input key coder input_key_coder does not match output key coder '
+        'output_key_coder for transform gbk')
+
+  def test_validate_pipeline_graph_reports_non_iterable_gbk_output(self):
+    pipeline = self._pipeline_with_gbk_coders(
+        output_values_coder_urn=common_urns.coders.BYTES.urn)
+
+    with self.assertRaises(ValueError) as error:
+      validate_pipeline_graph(pipeline)
+
+    self.assertEqual(
+        str(error.exception),
+        'Output value coder output_values_coder for transform gbk must be an '
+        'iterable coder, but uses URN beam:coder:bytes:v1')
+
+  def test_validate_pipeline_graph_reports_gbk_value_coder_mismatch(self):
+    pipeline = self._pipeline_with_gbk_coders(
+        input_value_coder='input_value_coder',
+        output_value_coder='output_value_coder')
+
+    with self.assertRaises(ValueError) as error:
+      validate_pipeline_graph(pipeline)
+
+    self.assertEqual(
+        str(error.exception),
+        'Input value coder input_value_coder does not match output value coder 
'
+        'output_value_coder for transform gbk')
+
   def test_equal_environments_merged(self):
     pipeline_proto = merge_common_environments(
         beam_runner_api_pb2.Pipeline(

Reply via email to