{"record":{"id":"f3c196f0180f149b","repo":"apache/beam","slug":"tvfnamelist-is-only-supported-for-change-streams-with","errorCode":null,"errorMessage":"tvfNameList is only supported for change streams with MUTABLE_KEY_RANGE mode","messagePattern":"tvfNameList is only supported for change streams with MUTABLE_KEY_RANGE mode","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java","lineNumber":2314,"sourceCode":"          getInclusiveEndAt().compareTo(MAX_INCLUSIVE_END_AT) > 0\n              ? MAX_INCLUSIVE_END_AT\n              : getInclusiveEndAt();\n      final List<String> tvfNameList = getTvfNameList();\n      final MapperFactory mapperFactory = new MapperFactory(changeStreamDatabaseDialect);\n      final ChangeStreamMetrics metrics = new ChangeStreamMetrics();\n      final RpcPriority rpcPriority = MoreObjects.firstNonNull(getRpcPriority(), RpcPriority.HIGH);\n      final SpannerAccessor spannerAccessor =\n          SpannerAccessor.getOrCreate(\n              changeStreamSpannerConfig,\n              input.getPipeline().getOptions().as(SdkHarnessOptions.class).getOpenTelemetry());\n      final boolean isMutableChangeStream =\n          isMutableChangeStream(\n              spannerAccessor.getDatabaseClient(), changeStreamDatabaseDialect, changeStreamName);\n      LOG.info(\"The change stream {} is mutable: {}\", changeStreamName, isMutableChangeStream);\n      List<String> quoteEscapedTvfNameList = null;\n      if (tvfNameList != null && !tvfNameList.isEmpty()) {\n        if (!isMutableChangeStream) {\n          throw new IllegalArgumentException(\n              \"tvfNameList is only supported for change streams with MUTABLE_KEY_RANGE mode\");\n        }\n        // TODO: if !per_placement_tvf=true, throw exception.\n        quoteEscapedTvfNameList = new ArrayList<>();\n        for (String tvfName : tvfNameList) {\n          quoteEscapedTvfNameList.add(escapeQuotes(tvfName));\n        }\n        checkTvfExistence(spannerAccessor.getDatabaseClient(), quoteEscapedTvfNameList);\n      }\n      final DaoFactory daoFactory =\n          new DaoFactory(\n              changeStreamSpannerConfig,\n              changeStreamName,\n              quoteEscapedTvfNameList,\n              partitionMetadataSpannerConfig,\n              partitionMetadataTableNames,\n              rpcPriority,\n              input.getPipeline().getOptions().getJobName(),","sourceCodeStart":2296,"sourceCodeEnd":2332,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIO.java#L2296-L2332","documentation":"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.","triggerScenarios":"Calling .withTvfNameList(...) (or equivalent option) on SpannerIO.readChangeStream() while the target change stream's mode is not MUTABLE_KEY_RANGE.","commonSituations":"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.","solutions":["Recreate the change stream with MODE MUTABLE_KEY_RANGE in Spanner, or","Remove the tvfNameList option when reading a non-mutable change stream","Verify the changeStreamName points to the intended change stream","Check the logged 'is mutable' line in pipeline logs to confirm the detected mode"],"exampleFix":"// before (non-mutable stream)\n.readChangeStream().withChangeStreamName(\"cs\").withTvfNameList(List.of(\"cs_tvf\"))\n// after\nCREATE CHANGE STREAM cs MODE MUTABLE_KEY_RANGE;  -- then use tvfNameList","handlingStrategy":"validation","validationCode":"if (tvfNameList != null && !tvfNameList.isEmpty() && !changeStreamIsMutable(dbClient, changeStreamName)) {\n  throw new IllegalArgumentException(\"tvfNameList requires MUTABLE_KEY_RANGE change stream\");\n}","typeGuard":null,"tryCatchPattern":"try { pipeline.apply(readChangeStream); } catch (IllegalArgumentException e) { /* fall back to non-TVF read or recreate stream */ }","preventionTips":["Create change streams with MODE MUTABLE_KEY_RANGE when using per-placement TVFs","Keep change stream definitions in migration scripts reviewed alongside pipeline config","Verify change stream mode before enabling tvfNameList"],"tags":["java","spanner","change-stream","feature-support"],"backgroundTag":"feature-not-enabled","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T21:17:11.552Z"}