apache/beam · error · IllegalArgumentException

tvfNameList is only supported for change streams with MUTABL

Error message

tvfNameList is only supported for change streams with MUTABLE_KEY_RANGE mode

What it means

When reading change streams, a list of table-valued-function (TVF) names (per-placement TVFs) may only be supplied if the change stream was created with MUTABLE_KEY_RANGE mode. SpannerIO checks isMutableChangeStream against the database and throws IllegalArgumentException if tvfNameList is provided for a non-mutable change stream.

Source

Thrown at sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java:2314

          getInclusiveEndAt().compareTo(MAX_INCLUSIVE_END_AT) > 0
              ? MAX_INCLUSIVE_END_AT
              : getInclusiveEndAt();
      final List<String> tvfNameList = getTvfNameList();
      final MapperFactory mapperFactory = new MapperFactory(changeStreamDatabaseDialect);
      final ChangeStreamMetrics metrics = new ChangeStreamMetrics();
      final RpcPriority rpcPriority = MoreObjects.firstNonNull(getRpcPriority(), RpcPriority.HIGH);
      final SpannerAccessor spannerAccessor =
          SpannerAccessor.getOrCreate(
              changeStreamSpannerConfig,
              input.getPipeline().getOptions().as(SdkHarnessOptions.class).getOpenTelemetry());
      final boolean isMutableChangeStream =
          isMutableChangeStream(
              spannerAccessor.getDatabaseClient(), changeStreamDatabaseDialect, changeStreamName);
      LOG.info("The change stream {} is mutable: {}", changeStreamName, isMutableChangeStream);
      List<String> quoteEscapedTvfNameList = null;
      if (tvfNameList != null && !tvfNameList.isEmpty()) {
        if (!isMutableChangeStream) {
          throw new IllegalArgumentException(
              "tvfNameList is only supported for change streams with MUTABLE_KEY_RANGE mode");
        }
        // TODO: if !per_placement_tvf=true, throw exception.
        quoteEscapedTvfNameList = new ArrayList<>();
        for (String tvfName : tvfNameList) {
          quoteEscapedTvfNameList.add(escapeQuotes(tvfName));
        }
        checkTvfExistence(spannerAccessor.getDatabaseClient(), quoteEscapedTvfNameList);
      }
      final DaoFactory daoFactory =
          new DaoFactory(
              changeStreamSpannerConfig,
              changeStreamName,
              quoteEscapedTvfNameList,
              partitionMetadataSpannerConfig,
              partitionMetadataTableNames,
              rpcPriority,
              input.getPipeline().getOptions().getJobName(),

View on GitHub (pinned to 12126d8942)

Solutions

  1. Recreate the change stream with MODE MUTABLE_KEY_RANGE in Spanner, or
  2. Remove the tvfNameList option when reading a non-mutable change stream
  3. Verify the changeStreamName points to the intended change stream
  4. Check the logged 'is mutable' line in pipeline logs to confirm the detected mode

Example fix

// before (non-mutable stream)
.readChangeStream().withChangeStreamName("cs").withTvfNameList(List.of("cs_tvf"))
// after
CREATE CHANGE STREAM cs MODE MUTABLE_KEY_RANGE;  -- then use tvfNameList
Defensive patterns

Strategy: validation

Validate before calling

if (tvfNameList != null && !tvfNameList.isEmpty() && !changeStreamIsMutable(dbClient, changeStreamName)) {
  throw new IllegalArgumentException("tvfNameList requires MUTABLE_KEY_RANGE change stream");
}

Try / catch

try { pipeline.apply(readChangeStream); } catch (IllegalArgumentException e) { /* fall back to non-TVF read or recreate stream */ }

Prevention

When it happens

Trigger: Calling .withTvfNameList(...) (or equivalent option) on SpannerIO.readChangeStream() while the target change stream's mode is not MUTABLE_KEY_RANGE.

Common situations: Reusing pipeline code written for mutable change streams against an older/standard change stream; migrating configs without recreating the change stream with MODE MUTABLE_KEY_RANGE; typos pointing at the wrong change stream name.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/f3c196f0180f149b. Report an issue: GitHub.