{"record":{"id":"8a65be89c557bd02","repo":"apache/beam","slug":"query-and-table-params-are-mutually-exclusive-set-just-one","errorCode":null,"errorMessage":"Query and table params are mutually exclusive. Set just one of them.","messagePattern":"Query and table params are mutually exclusive\\. Set just one of them\\.","errorType":"validation","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java","lineNumber":224,"sourceCode":"                ? TimestampBound.ofMaxStaleness(staleness, TimeUnit.valueOf(timeUnit))\n                : TimestampBound.ofExactStaleness(staleness, TimeUnit.valueOf(timeUnit));\n          case READ_TIMESTAMP:\n          case MIN_READ_TIMESTAMP:\n            if (readTimestamp == null) {\n              throw new NullPointerException(\n                  \"Timestamp cannot be null when READ_TIMESTAMP or MIN_READ_TIMESTAMP mode is selected\");\n            }\n            return mode == READ_TIMESTAMP\n                ? TimestampBound.ofReadTimestamp(Timestamp.parseTimestamp(readTimestamp))\n                : TimestampBound.ofMinReadTimestamp(Timestamp.parseTimestamp(readTimestamp));\n          default:\n            throw new IllegalArgumentException(\"Unknown timestamp bound mode: \" + mode);\n        }\n      }\n\n      public ReadOperation getReadOperation() {\n        if (sql != null && table != null) {\n          throw new IllegalStateException(\n              \"Query and table params are mutually exclusive. Set just one of them.\");\n        }\n        ReadOperation readOperation = ReadOperation.create();\n        if (sql != null) {\n          return readOperation.withQuery(sql);\n        }\n        if (Schema.builder().build().equals(schema)) {\n          throw new IllegalArgumentException(\"Schema can't be empty\");\n        }\n        if (table != null) {\n          return readOperation.withTable(table).withColumns(schema.getFieldNames());\n        }\n        throw new IllegalStateException(\"Can't happen\");\n      }\n    }\n\n    @Override\n    @NonNull","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java#L206-L242","documentation":"getReadOperation() builds the Spanner ReadOperation and throws IllegalStateException when both a SQL query (sql) and a table name (table) are set, because a Spanner read must originate either from a query or from a table+columns read, never both. The two sources of rows are mutually exclusive by design.","triggerScenarios":"Calling SpannerIO.read().withQuery(\"SELECT ...\").withTable(\"my_table\") (directly or via configuration where both sql and table keys are present), or merging defaults that set table while user config sets sql.","commonSituations":"External pipeline templates that pre-populate table and the user adds a query; copy-pasted builder chains accumulating both setters; config merge (base + override) keeping both keys.","solutions":["Remove either .withQuery(...) or .withTable(...) from the builder, keeping only one.","If using table reads, drop sql and rely on withTable + schema columns; for arbitrary reads use withQuery and unset table.","When merging configs, explicitly null out the unused field instead of keeping both."],"exampleFix":"// before\nSpannerIO.read().withQuery(\"SELECT * FROM t\").withTable(\"t\")\n// after\nSpannerIO.read().withQuery(\"SELECT * FROM t\")","handlingStrategy":"validation","validationCode":"if (sql != null && table != null) { throw new IllegalArgumentException(\"Set either sql or table, not both\"); }","typeGuard":null,"tryCatchPattern":"try { ReadOperation op = config.getReadOperation(); } catch (IllegalStateException e) { throw new ConfigException(\"Spanner read source over-specified: \" + e.getMessage(), e); }","preventionTips":["Use a builder helper that clears the table when a query is set (and vice versa).","When merging base/override configs, null out the unused field.","Decide read mode (query vs table) explicitly at config schema level, e.g. a oneOf."],"tags":["java","apache-beam","google-cloud-spanner","validation"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}