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
    @NonNull

View on GitHub (pinned to 12126d8942)

Solutions

  1. Remove either .withQuery(...) or .withTable(...) from the builder, keeping only one.
  2. If using table reads, drop sql and rely on withTable + schema columns; for arbitrary reads use withQuery and unset table.
  3. 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

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


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