{"record":{"id":"94cf3216003dd647","repo":"apache/cassandra","slug":"counter-and-non-counter-mutations-cannot-exist-in","errorCode":null,"errorMessage":"Counter and non-counter mutations cannot exist in the same batch","messagePattern":"Counter and non-counter mutations cannot exist in the same batch","errorType":"validation","errorClass":"InvalidRequestException","httpStatus":null,"severity":"error","filePath":"src/java/org/apache/cassandra/cql3/statements/BatchStatement.java","lineNumber":297,"sourceCode":"            if (statement.isCounter())\n                hasCounters = true;\n            else\n                hasNonCounters = true;\n\n            if (statement.isVirtual())\n                hasVirtualTables = true;\n            else\n                hasRegularTables = true;\n        }\n\n        if (timestampSet && hasCounters)\n            throw new InvalidRequestException(\"Cannot provide custom timestamp for a BATCH containing counters\");\n\n        if (isCounter() && hasNonCounters)\n            throw new InvalidRequestException(\"Cannot include non-counter statement in a counter batch\");\n\n        if (hasCounters && hasNonCounters)\n            throw new InvalidRequestException(\"Counter and non-counter mutations cannot exist in the same batch\");\n\n        if (isLogged() && hasCounters)\n            throw new InvalidRequestException(\"Cannot include a counter statement in a logged batch\");\n\n        if (isLogged() && hasVirtualTables)\n            throw new InvalidRequestException(\"Cannot include a virtual table statement in a logged batch\");\n\n        if (hasVirtualTables && hasRegularTables)\n            throw new InvalidRequestException(\"Mutations for virtual and regular tables cannot exist in the same batch\");\n\n        if (hasConditions && hasVirtualTables)\n            throw new InvalidRequestException(\"Conditional BATCH statements cannot include mutations for virtual tables\");\n\n        if (hasConditions)\n        {\n            String ksName = null;\n            String cfName = null;\n            for (ModificationStatement stmt : statements)","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/apache/cassandra/blob/88fd0f6a0eaed8943f05ac9e8f947882b8ddc8f1/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java#L279-L315","documentation":"Cassandra batches must be homogeneous with respect to counter mutations: counters use a different replication/update path than regular writes, so a single BATCH cannot mix counter and non-counter statements. BatchStatement.validate() detects the combination of hasCounters && hasNonCounters and rejects the batch before execution with InvalidRequestException.","triggerScenarios":"A single BATCH statement contains at least one INSERT/UPDATE/DELETE on a table with a counter column and at least one mutation on a regular (non-counter) table, e.g. 'BEGIN BATCH UPDATE counters SET c=c+1 ...; INSERT INTO regular ...; APPLY BATCH'.","commonSituations":"Applications that batch related writes across tables where one table was later migrated to counters; ORM/query builders that assemble batches dynamically; users unaware counters can't be batched with normal writes.","solutions":["Split into two separate BATCH statements: one for counter mutations, one for regular mutations","Execute the counter mutation and the regular mutation as independent statements in the same logical transaction of the application","Redesign the schema if counter and regular data must be written atomically (counters cannot participate in atomic batches anyway)"],"exampleFix":"// before\nBEGIN BATCH\n  UPDATE page_counts SET views = views + 1 WHERE page = 'home';\n  INSERT INTO events (id, data) VALUES (1, 'x');\nAPPLY BATCH;\n// after\nUPDATE page_counts SET views = views + 1 WHERE page = 'home';\nBEGIN BATCH INSERT INTO events (id, data) VALUES (1, 'x'); APPLY BATCH;","handlingStrategy":"validation","validationCode":"boolean hasCounter = tables.stream().anyMatch(t -> isCounterTable(t));\nboolean hasRegular = tables.stream().anyMatch(t -> !isCounterTable(t));\nif (hasCounter && hasRegular) throw new IllegalArgumentException(\"Split batch: counters and non-counters cannot share a BATCH\");","typeGuard":null,"tryCatchPattern":"catch (InvalidRequestException e) { if (e.getMessage().contains(\"Counter and non-counter\")) { splitAndRetrySeparately(); } else throw e; }","preventionTips":["Track which of your tables are counter tables and route their writes through separate batch builders","Keep counter mutations in dedicated code paths, never in generic batch utilities","Add unit tests that assert batch builders never mix table kinds"],"tags":["cql","batch","counter","invalid-request"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"88fd0f6a0eaed8943f05ac9e8f947882b8ddc8f1","analyzedAt":"2026-09-10T07:29:22.284Z","contentChangedAt":"2026-09-10T07:29:22.284Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}