apache/beam · error · IllegalStateException
Query and table params are mutually exclusive. Set just one
Error message
Query and table params are mutually exclusive. Set just one of them.
What it means
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.
Source
Thrown at sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java:224
? TimestampBound.ofMaxStaleness(staleness, TimeUnit.valueOf(timeUnit))
: TimestampBound.ofExactStaleness(staleness, TimeUnit.valueOf(timeUnit));
case READ_TIMESTAMP:
case MIN_READ_TIMESTAMP:
if (readTimestamp == null) {
throw new NullPointerException(
"Timestamp cannot be null when READ_TIMESTAMP or MIN_READ_TIMESTAMP mode is selected");
}
return mode == READ_TIMESTAMP
? TimestampBound.ofReadTimestamp(Timestamp.parseTimestamp(readTimestamp))
: TimestampBound.ofMinReadTimestamp(Timestamp.parseTimestamp(readTimestamp));
default:
throw new IllegalArgumentException("Unknown timestamp bound mode: " + mode);
}
}
public ReadOperation getReadOperation() {
if (sql != null && table != null) {
throw new IllegalStateException(
"Query and table params are mutually exclusive. Set just one of them.");
}
ReadOperation readOperation = ReadOperation.create();
if (sql != null) {
return readOperation.withQuery(sql);
}
if (Schema.builder().build().equals(schema)) {
throw new IllegalArgumentException("Schema can't be empty");
}
if (table != null) {
return readOperation.withTable(table).withColumns(schema.getFieldNames());
}
throw new IllegalStateException("Can't happen");
}
}
@Override
@NonNullView on GitHub (pinned to 12126d8942)
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.
Example fix
// before
SpannerIO.read().withQuery("SELECT * FROM t").withTable("t")
// after
SpannerIO.read().withQuery("SELECT * FROM t") Defensive patterns
Strategy: validation
Validate before calling
if (sql != null && table != null) { throw new IllegalArgumentException("Set either sql or table, not both"); } Try / catch
try { ReadOperation op = config.getReadOperation(); } catch (IllegalStateException e) { throw new ConfigException("Spanner read source over-specified: " + e.getMessage(), e); } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- databaseId can't be empty
- instanceId can't be empty
- Both clientCertPath and clientCertKeyPath must be specified
- Schema can't be empty
- ChangeStreamName can't be empty
AI-assisted analysis of apache/beam@12126d8942 (2026-09-13).
Data as JSON: /api/errors/8a65be89c557bd02.
Report an issue: GitHub.