apache/cassandra · error · InvalidRequestException

Mutations for virtual and regular tables cannot exist in the

Error message

Mutations for virtual and regular tables cannot exist in the same batch

What it means

A single batch cannot contain mutations targeting both virtual tables and regular tables; the two write paths are incompatible. validate() flags the combination of hasVirtualTables && hasRegularTables and rejects the batch.

Source

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

        }

        if (timestampSet && hasCounters)
            throw new InvalidRequestException("Cannot provide custom timestamp for a BATCH containing counters");

        if (isCounter() && hasNonCounters)
            throw new InvalidRequestException("Cannot include non-counter statement in a counter batch");

        if (hasCounters && hasNonCounters)
            throw new InvalidRequestException("Counter and non-counter mutations cannot exist in the same batch");

        if (isLogged() && hasCounters)
            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();
            }
        }
    }

View on GitHub (pinned to 88fd0f6a0e)

Solutions

  1. Issue separate statements/batches for virtual and regular table operations
  2. Filter virtual tables out of batch-building code paths (query system_schema / metadata to detect them)
  3. Remember virtual tables are effectively read-only; remove those mutations entirely

Example fix

// before
BEGIN BATCH INSERT INTO ks.t ...; UPDATE system_views.v ...; APPLY BATCH;
// after
INSERT INTO ks.t ...; -- virtual table mutation removed
Defensive patterns

Strategy: validation

Validate before calling

boolean anyVirtual = stmts.stream().anyMatch(s -> s.getTable().isVirtual());
boolean anyRegular = stmts.stream().anyMatch(s -> !s.getTable().isVirtual());
if (anyVirtual && anyRegular) throw new IllegalArgumentException("Separate virtual and regular table writes");

Type guard

boolean isVirtualTable(TableMetadata tm) { return tm.isVirtual(); }

Try / catch

catch (InvalidRequestException e) { if (e.getMessage().contains("virtual and regular")) { partitionStatementsByKindAndRetry(); } else throw e; }

Prevention

When it happens

Trigger: A BATCH mixing e.g. 'UPDATE system_views.x ...' with 'INSERT INTO my_keyspace.my_table ...'.

Common situations: Generated or templated batches that iterate a table list containing both virtual and regular tables; tooling that doesn't distinguish virtual tables.

Related errors


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