{"record":{"id":"a5ef629e5655f4f9","repo":"apache/beam","slug":"read-operation-in-the-constructor-only-works-with-the-root","errorCode":null,"errorMessage":"Read operation in the constructor only works with the root of the pipeline.","messagePattern":"Read operation in the constructor only works with the root of the pipeline\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/io/gcp/experimental/spannerio.py","lineNumber":771,"sourceCode":"      if table is not None:\n        if columns is None:\n          raise ValueError(\"Columns are required with the table name.\")\n        self._read_operations = [\n            ReadOperation.table(\n                table=table, columns=columns, index=index, keyset=keyset)\n        ]\n      elif sql is not None:\n        self._read_operations = [\n            ReadOperation.query(\n                sql=sql, params=params, param_types=param_types)\n        ]\n\n  def expand(self, pbegin):\n    if self._read_operations is not None and isinstance(pbegin, PBegin):\n      pcoll = pbegin.pipeline | Create(self._read_operations)\n    elif not isinstance(pbegin, PBegin):\n      if self._read_operations is not None:\n        raise ValueError(\n            \"Read operation in the constructor only works with \"\n            \"the root of the pipeline.\")\n      pcoll = pbegin\n    else:\n      raise ValueError(\n          \"Spanner required read operation, sql or table \"\n          \"with columns.\")\n\n    if self._transaction is None:\n      # reading as batch read using the spanner partitioning query to create\n      # batches.\n      p = (\n          pcoll\n          | 'Generate Partitions' >> ParDo(\n              _CreateReadPartitions(spanner_configuration=self._configuration))\n          | 'Reshuffle' >> Reshuffle()\n          | 'Read From Partitions' >> ParDo(\n              _ReadFromPartitionFn(spanner_configuration=self._configuration)))","sourceCodeStart":753,"sourceCodeEnd":789,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/io/gcp/experimental/spannerio.py#L753-L789","documentation":"SpannerIO's Read ReadFromSpanner transform supports pre-built ReadOperation objects only when the transform is the root of the pipeline (input is PBegin). If the transform receives a real PCollection as input while read operations were also supplied in the constructor, the library raises this ValueError because both sources of read configuration conflict.","triggerScenarios":"Calling beam.Pipeline | ReadFromSpanner(..., read_operations=[...]) applied to an existing PCollection instead of pbegin, e.g. piping ReadFromSpanner after another transform while also passing read_operations/sql/table to the constructor.","commonSituations":"Developers chain ReadFromSpanner into an existing pipeline after another step (e.g. reading side inputs) but forget that constructor-supplied read_operations only work at pipeline root; often after refactoring a standalone read into a larger pipeline.","solutions":["Do not pass read_operations (or sql/table) to ReadFromSpanner when piping from an existing PCollection; use SpannerIO.Read with an input PCollection of ReadOperation objects instead.","Apply the transform to the pipeline root: p | ReadFromSpanner(project=..., instance=..., database=..., read_operations=[...]).","If you need per-element reads, build ReadOperation objects upstream and pipe them into SpannerRead with an explicit input PCollection."],"exampleFix":"# before\npcoll | ReadFromSpanner(project='p', instance='i', database='d', read_operations=[op])\n# after\np | ReadFromSpanner(project='p', instance='i', database='d', read_operations=[op])","handlingStrategy":"validation","validationCode":"def use_read_ops_at_root(read_operations, has_input_pcoll):\n    if read_operations and has_input_pcoll:\n        raise ValueError('read_operations only allowed when ReadFromSpanner is applied to pipeline root (PBegin)')\n    return True","typeGuard":"def is_pipeline_root(pbegin_or_coll):\n    from apache_beam import PBegin\n    return isinstance(pbegin_or_coll, PBegin)","tryCatchPattern":null,"preventionTips":["Never pass read_operations/sql/table to ReadFromSpanner unless applied directly to the Pipeline object","Use SpannerRead with a PCollection of ReadOperation for non-root reads","Pin a check in code review when refactoring reads out of the root"],"tags":["apache-beam","google-cloud-spanner","pipeline-structure","valueerror"],"backgroundTag":"invalid-argument-value","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-20T03:17:13.778Z"}