CQL Transaction Reference (BEGIN TRANSACTION)
|
Preview | Unofficial | For review only This page documents preview transaction syntax and behavior. Treat it as draft reference material. |
This page documents the BEGIN TRANSACTION CQL statement introduced in Apache Cassandra 6 as part of the Accord distributed transaction system (CEP-15, Accord).
It is a user-facing reference for the syntax and semantics of writing multi-partition atomic transactions in CQL.
For the operational guide covering table configuration, migration, and consistency level semantics, see Onboarding to Accord. For the internal architecture of the Accord protocol, see Accord Architecture. For the CQL-to-Accord implementation internals, see Developers guide to CQL on Accord.
BEGIN TRANSACTION requires Accord to be enabled (accord.enabled: true in cassandra.yaml) and the target tables to have transactional_mode = 'full' or transactional_mode = 'mixed_reads'.
See Onboarding to Accord for enablement steps.
|
|
Do not use prepared statements for |
Prerequisites
Before using BEGIN TRANSACTION:
-
Accord must be enabled in
cassandra.yaml:accord.enabled: true. Source:conf/cassandra.yamllines 2737-2754. -
Every table accessed inside the transaction must have
transactional_mode = 'full'set as a table property (or'mixed_reads'for tables where non-transactional reads are also needed). Source:TransactionStatement.javaTRANSACTIONS_DISABLED_ON_TABLE_MESSAGE. -
BEGIN TRANSACTIONstatements cannot be used as prepared statements. Source:TransactionStatement.javaeligibleAsPreparedStatement()returnsfalse.
Statement Overview
A BEGIN TRANSACTION block is an atomic, isolated CQL transaction coordinated by the Accord protocol.
It can span multiple partitions and multiple tables.
Outside a transaction, Cassandra follows its normal eventually consistent replica coordination model for regular CQL statements.
Inside a transaction, Accord provides serializable isolation for the reads and writes in the block.
Three patterns are supported:
| Read-only |
One or more |
| Write-only |
One or more |
| Conditional read-write |
|
BNF Grammar
The grammar below is derived from the ANTLR parser at src/antlr/Parser.g lines 780-798.
transaction_statement
: BEGIN TRANSACTION
let_assignment*
( returning_select | reference_select )?
( IF txn_conditions THEN )?
modification_statement*
( END IF )?
COMMIT TRANSACTION
;
let_assignment
: LET identifier '=' '(' SELECT let_selectors
FROM table_name WHERE where_clause
( LIMIT integer )? ')' ';'
;
let_selectors
: '*'
| let_selector ( ',' let_selector )*
;
returning_select
: SELECT selectors FROM table_name WHERE where_clause ';'
;
reference_select
: SELECT row_data_reference ( ',' row_data_reference )* ';'
;
row_data_reference
: identifier '.' column_reference
;
txn_conditions
: txn_column_condition ( AND txn_column_condition )*
;
txn_column_condition
: row_data_reference IS NOT NULL
| row_data_reference IS NULL
| row_data_reference txn_condition_op term
| term txn_condition_op row_data_reference
;
txn_condition_op
: '=' | '<' | '<=' | '>' | '>=' | '!='
;
modification_statement
: ( insert_statement | update_statement | delete_statement ) ';'
;
Source: src/antlr/Parser.g lines 350-372 (letStatement), 780-850 (batchTxnStatement, txnConditions, txnColumnCondition, txnConditionKind), 750-754 (batchStatementObjective).
When the transaction contains a conditional (IF … THEN), the closing delimiter is END IF followed immediately by COMMIT TRANSACTION, not END IF COMMIT TRANSACTION on separate lines.
Source: parser rule line 793: {isTxnConditional}? (K_END K_IF K_COMMIT K_TRANSACTION).
|
Statement Types and Their Roles
LET Assignments
LET statements declare named row-variables by executing a SELECT at transaction read time.
The bound identifier can then be referenced in conditions and write expressions.
Syntax:
LET <name> = (SELECT <columns> FROM <table> WHERE <where_clause> [LIMIT <n>]);
Rules derived from TransactionStatement.java:
-
Each
LETname must be unique within the transaction. -
The
SELECTinsideLETmust specify all partition key columns with equality operators and must identify at most one row (either full primary key or partition key +LIMIT 1). Partition range queries are not allowed insideLET. -
ORDER BYandGROUP BYare not allowed insideLETselects. -
Aggregation functions are not allowed.
-
Counter columns are not allowed.
-
When the partition key is an
INclause,LIMITmust not be present.
These restrictions keep each LET read bounded and deterministic for Accord.
If you need a wider read, do it outside the transaction and pass the resulting values back in as bind variables.
If you need multiple rows, split the workflow into separate reads or redesign the request around a single partition-key lookup.
Returning SELECT
A SELECT statement inside the transaction body returns data to the caller as the transaction result.
Only one returning select is allowed per transaction, and it cannot be combined with a reference select (SELECT <ref>.<col>).
Rules:
-
The
SELECTmust specify all partition key columns with equality operators. -
Partition range queries are not allowed.
-
Multiple partitions can be targeted via an
INclause, butLIMITis not allowed when the partition key is anINclause.
Reference SELECT
Instead of a full SELECT, you can return specific columns from LET-bound rows:
SELECT <name>.<col> [, <name>.<col> ...];
Each reference must specify a column name (row1.v); selecting an entire row variable (SELECT row1) is not permitted.
Source: TransactionStatement.java SELECT_REFS_NEED_COLUMN_MESSAGE.
IF Conditions
The IF block tests conditions derived from LET-bound row references.
It is optional; if absent, writes execute unconditionally.
Supported condition forms (source: Parser.g lines 839-850):
IF <ref> IS NOT NULL -- row or column exists
AND <ref> IS NULL -- row or column is null / absent
AND <ref> <op> <literal> -- comparison: =, <, <=, >, >=, !=
AND <literal> <op> <ref> -- comparison with literal on left
THEN
Conditions are joined with AND; OR is not supported.
Modification Statements (INSERT / UPDATE / DELETE)
INSERT, UPDATE, and DELETE statements are the write operations within a transaction.
They are subject to the following restrictions (source: TransactionStatement.java lines 767-774):
-
IF [NOT] EXISTSand other CAS conditions are not allowed on individual modification statements inside a transaction. Use the transaction-levelIF … THENblock instead. -
USING TIMESTAMPis not allowed on writes inside a transaction. The timestamp is assigned by Accord. -
USING TTLis not allowed. -
Counter columns are not allowed.
-
The target table must have Accord enabled (
transactional_mode != 'off').
Write values can reference LET-bound column values using <name>.<column> notation (read-before-write pattern).
Examples
Write-only transaction (unconditional, multi-partition)
BEGIN TRANSACTION
INSERT INTO purchases (user_id, order_id, total) VALUES (42, 1001, 99);
UPDATE inventory SET stock = stock - 1 WHERE product_id = 7;
COMMIT TRANSACTION
Both writes commit atomically across partitions.
Read-only transaction
BEGIN TRANSACTION
SELECT user_id, balance FROM accounts WHERE user_id = 42;
COMMIT TRANSACTION
Returns the balance under serializable isolation.
Conditional update (check-and-set across partitions)
BEGIN TRANSACTION
LET row1 = (SELECT * FROM accounts WHERE user_id = 1);
LET row2 = (SELECT * FROM accounts WHERE user_id = 2);
SELECT row1.balance, row2.balance;
IF row1.balance >= 100 THEN
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
END IF
COMMIT TRANSACTION
Reads both accounts, returns the balances, and transfers funds only if the sender has sufficient balance. All reads and writes commit atomically.
Source pattern: Parser.g lines 759-766 (comment example).
Read-write with reference (read current value, write derived value)
BEGIN TRANSACTION
LET current = (SELECT * FROM counters WHERE id = 'pageviews');
SELECT current.count;
IF current IS NOT NULL THEN
UPDATE counters SET count = current.count + 1 WHERE id = 'pageviews';
END IF
COMMIT TRANSACTION
Reads the current count and increments it conditionally.
Source: PreparedStatementsTest.java lines 800-810.
Consistency Semantics
Outside a transaction, Cassandra’s consistency behavior is governed by the statement’s consistency level and normal replica coordination rules.
Inside a BEGIN TRANSACTION block, Accord provides serializable isolation.
All reads within a transaction observe a consistent snapshot as of the transaction’s executeAt timestamp, and all writes are committed atomically.
Supported Consistency Levels
The consistency level passed to the transaction controls interoperability behavior during migration.
Source: IAccordService.java lines 79-80.
| Direction | Supported Levels |
|---|---|
Read / commit |
|
Write / commit |
|
|
The following consistency levels are not supported by Accord: |
When ONE is specified as the write consistency level, Accord silently performs the commit at QUORUM.
Source: CQL on Accord, "Supported consistency levels" section.
The effective consistency level also depends on the table’s transactional_mode and its migration state.
See Onboarding to Accord for the full consistency semantics table.
Isolation
Accord provides serializable isolation for transactions.
All reads within a transaction execute at a single consistent executeAt timestamp determined by the Accord protocol.
Source: TransactionStatement.java createTxn method.
Restrictions
The following are not permitted inside a BEGIN TRANSACTION block.
All restrictions are derived from TransactionStatement.java.
| Restriction | Error message (truncated) |
|---|---|
Empty transaction (no reads or writes) |
|
|
|
|
|
|
|
Counter column access (read or write) |
|
Aggregation functions in |
|
|
|
|
|
Partition range query in |
|
Incomplete partition key in |
|
Duplicate |
|
|
|
|
|
Selecting entire LET row variable without column ( |
|
Table with |
|
Accord globally disabled ( |
|
Using |
|
Transaction with no SELECT and no writes after conditional evaluation |
Write operations are silently ignored (logged at WARN level); source: |
Multi-partition transaction while table is migrating away from Accord |
|
Error Conditions
At Parse Time (SyntaxException)
-
IF … THENblock without closingEND IF: parse fails withfailed predicate. -
END IFwithout a precedingIF: parse fails withfailed predicate. -
Referencing
<name>.<column>outside a transaction (noBEGIN TRANSACTIONcontext): parse fails.
At Prepare Time (InvalidRequestException)
All checkFalse/checkTrue/checkNotNull assertions in TransactionStatement.Parsed.prepare() raise InvalidRequestException at statement preparation.
See the restrictions table above for the corresponding messages.
At Execution Time (InvalidRequestException)
-
Accord disabled globally: raised at execution.
-
Table migrating away from Accord:
UNSUPPORTED_MIGRATIONraised asInvalidRequestException. -
Validation rejection from the Accord protocol (e.g., constraint violations propagated from replicas): raised via
TxnValidationRejection. Source:TransactionStatement.javaline 566.
|
As of CASSANDRA-21061 (included in 6.0-alpha1), Accord write rejections are returned as |
Consistency Level Errors
Unsupported consistency levels (LOCAL_QUORUM, TWO, etc.) are rejected at execution time by Accord.
Source: IAccordService.java.
Prepared Statements
Do not use prepared statements for BEGIN TRANSACTION blocks.
The server marks them as not eligible for prepared-statement caching, so this reference treats them as plain request text only.
Source: TransactionStatement.java eligibleAsPreparedStatement() returns false.
Bind Variables
Bind variables (?) are supported within the statements inside a transaction body, following the same rules as standalone CQL statements.
Source: PreparedStatementsTest.java lines 243-249 (SELECT with WHERE a = ?), lines 806-808 (LET and UPDATE with WHERE pk = ?).
Audit Logging
BEGIN TRANSACTION statements are logged with audit entry type TRANSACTION.
Source: TransactionStatement.java getAuditLogContext().
Permission Model
Authorization for a transaction checks:
-
SELECTpermission for every table referenced inLETand returningSELECTstatements. -
Write permission (
MODIFY) for every table referenced inINSERT,UPDATE, andDELETEstatements.
Source: TransactionStatement.java authorize(ClientState state).
Related Pages
-
Accord — Overview of the Accord transaction protocol.
-
Accord Architecture — Internal implementation details for contributors.
-
Developers guide to CQL on Accord — How CQL statements map to Accord transactions internally.
-
Onboarding to Accord — Operator guide: enabling Accord, configuring tables, migration procedures.
-
Data Manipulation Language — Standard INSERT, UPDATE, DELETE, SELECT reference.
Unresolved Questions
The following items require verification or additional research before this page can be promoted from draft status:
-
Paging semantics: Whether each page of a paged result constitutes a separate transaction is inferred but not explicitly documented in the grammar or TransactionStatement. Confirm with a contributor.
-
CASSANDRA-20883 binary protocol conditions: The triage file notes binary protocol multiple conditions for transactions. The exact new error codes or result types introduced by this JIRA have not been investigated.
-
accord.enableddefault: Thecassandra.yamldefault foraccord.enabledisfalseper the research file. Verify this is still the case in the final 6.0 release before publishing this page. -
transactional_mode = 'mixed_reads'in transactions: Whether a table withmixed_readscan participate in aBEGIN TRANSACTIONblock (as opposed to only having non-transactional reads routed outside Accord) should be clarified. -
Multi-table transaction key type restriction: Accord supports only key transactions or range transactions, not both within the same transaction. The user-visible restriction (if any, beyond partition range queries being rejected) should be documented explicitly.