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
- Issue separate statements/batches for virtual and regular table operations
- Filter virtual tables out of batch-building code paths (query system_schema / metadata to detect them)
- 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
- Filter virtual tables out at the data-source layer before any batching
- Treat virtual tables as read-only everywhere in your codebase
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
- Cannot include a virtual table statement in a logged batch
- Conditional BATCH statements cannot include mutations for vi
- Global TTL on the BATCH statement is not supported.
- Cannot provide custom timestamp for conditional BATCH
- Cannot provide custom timestamp for counter BATCH
AI-assisted analysis of apache/cassandra@88fd0f6a0e (2026-09-10).
Data as JSON: /api/errors/688aa32d98de5c47.
Report an issue: GitHub.