apache/cassandra · error · InvalidRequestException

Batch with conditions cannot span multiple tables: %s

Error message

Batch with conditions cannot span multiple tables: %s

What it means

Batches using lightweight transactions (conditions) are only atomic within a single table. When validate() finds conditional statements targeting more than one keyspace/table, it rejects the batch, reporting the offending statement's source location.

Source

Thrown at src/java/org/apache/cassandra/cql3/statements/BatchStatement.java:318

            throw new InvalidRequestException("Cannot include a counter statement in a logged batch");

        if (isLogged() && hasVirtualTables)
            throw new InvalidRequestException("Cannot include a virtual table statement in a logged batch");

        if (hasVirtualTables && hasRegularTables)
            throw new InvalidRequestException("Mutations for virtual and regular tables cannot exist in the same batch");

        if (hasConditions && hasVirtualTables)
            throw new InvalidRequestException("Conditional BATCH statements cannot include mutations for virtual tables");

        if (hasConditions)
        {
            String ksName = null;
            String cfName = null;
            for (ModificationStatement stmt : statements)
            {
                if (ksName != null && (!stmt.keyspace().equals(ksName) || !stmt.table().equals(cfName)))
                    throw new InvalidRequestException("Batch with conditions cannot span multiple tables: " + stmt.source);
                ksName = stmt.keyspace();
                cfName = stmt.table();
            }
        }
    }

    private boolean isCounter()
    {
        return type == Type.COUNTER;
    }

    private boolean isLogged()
    {
        return type == Type.LOGGED;
    }

    // The batch itself will be validated in either Parsed#prepare() - for regular CQL3 batches,
    //   or in QueryProcessor.processBatch() - for native protocol batches.

View on GitHub (pinned to 88fd0f6a0e)

Solutions

  1. Restrict the conditional batch so all statements target one table (and as of the partition check, ideally one partition)
  2. Split into per-table conditional statements and handle cross-table coordination in the application
  3. Use a single denormalized table if atomic conditional updates across what are now multiple tables are required

Example fix

// before
BEGIN BATCH UPDATE users SET name='a' WHERE id=1 IF name='b';
             UPDATE orders SET status='shipped' WHERE id=9 IF status='paid'; APPLY BATCH;
// after
BEGIN BATCH UPDATE users SET name='a' WHERE id=1 IF name='b'; APPLY BATCH;
UPDATE orders SET status='shipped' WHERE id=9 IF status='paid';
Defensive patterns

Strategy: validation

Validate before calling

if (batchHasConditions && stmts.stream().map(s -> s.getTable()).distinct().count() > 1)
    throw new IllegalArgumentException("LWT batch must target a single table");

Try / catch

catch (InvalidRequestException e) { if (e.getMessage().startsWith("Batch with conditions cannot span multiple tables")) { splitPerTableAndRetry(); } else throw e; }

Prevention

When it happens

Trigger: 'BEGIN BATCH UPDATE t1 ... IF x = 1; UPDATE t2 ... IF y = 2; APPLY BATCH' where t1 and t2 are different tables (or different keyspaces).

Common situations: Multi-entity transaction attempts ported from relational databases; applications assuming CAS batches span tables.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of apache/cassandra@88fd0f6a0e (2026-09-10). Data as JSON: /api/errors/8a7484b4b39f54b0. Report an issue: GitHub.