mirror of https://github.com/apache/cassandra
Add user docs for Accord focused on how to use, what CQL is and isnt valid, and use cases
patch by David Capwell; reviewed by Caleb Rackliffe for CASSANDRA-21127
This commit is contained in:
parent
c9d468bf3c
commit
998f42984f
|
|
@ -43,6 +43,9 @@
|
|||
*** xref:cassandra:developing/cql/types.adoc[Data types]
|
||||
*** xref:cassandra:developing/cql/ddl.adoc[Data definition (DDL)]
|
||||
*** xref:cassandra:developing/cql/dml.adoc[Data manipulation (DML)]
|
||||
*** xref:cassandra:developing/cql/transactions.adoc[Transactions]
|
||||
*** xref:cassandra:developing/cql/transactions-examples.adoc[Transaction patterns]
|
||||
*** xref:cassandra:developing/cql/transactions-migration.adoc[Transaction migration]
|
||||
*** xref:cassandra:developing/cql/dynamic-data-masking.adoc[]
|
||||
*** xref:cassandra:developing/cql/operators.adoc[Operators]
|
||||
*** xref:cassandra:developing/cql/indexing/indexing-concepts.adoc[]
|
||||
|
|
|
|||
|
|
@ -474,3 +474,97 @@ partly applied.
|
|||
|
||||
Use the `COUNTER` option for batched counter updates. Unlike other
|
||||
updates in Cassandra, counter updates are not idempotent.
|
||||
|
||||
=== BATCH vs Transactions
|
||||
|
||||
BATCH statements have limitations compared to Accord transactions. Understanding the differences helps you choose the right approach for your use case.
|
||||
|
||||
==== BATCH Statement Capabilities
|
||||
|
||||
BATCH statements offer:
|
||||
|
||||
* **Single-partition atomicity**: All operations within a single partition succeed or fail together atomically
|
||||
* **Multi-partition batching**: LOGGED batches can span multiple partitions, but are not atomic. The batch log ensures all operations eventually complete, but there can be a window where only some operations are visible.
|
||||
* **Single partition isolation**: Operations on the same partition are isolated
|
||||
* **Network efficiency**: Multiple operations in one round-trip
|
||||
* **Automatic timestamps**: All operations use the same timestamp by default
|
||||
* **Conditional updates**: Batches support `IF` clauses for lightweight transactions (LWT/CAS) on a single partition. If any condition fails, the entire batch is rejected.
|
||||
* **Basic business rule enforcement**: `IF` clauses can validate conditions before applying changes, though limited to what CAS syntax supports
|
||||
|
||||
==== BATCH Statement Limitations
|
||||
|
||||
However, BATCH statements cannot provide:
|
||||
|
||||
* **Cross-partition atomicity**: Multi-partition LOGGED batches are not truly atomic (see above)
|
||||
* **Read-before-write patterns**: Cannot read data within the batch to make decisions based on current values
|
||||
* **Cross-partition conditional updates**: Conditional batches (using `IF` clauses) must operate on a single partition and table
|
||||
* **Complex conditional logic**: `IF` clauses are limited to simple equality/inequality checks on existing column values
|
||||
|
||||
==== When to Use Accord Transactions Instead
|
||||
|
||||
Consider using xref:developing/cql/transactions.adoc[Accord transactions] when you need:
|
||||
|
||||
**Read-Modify-Write Patterns:**
|
||||
[source,cql]
|
||||
----
|
||||
-- BATCH cannot check balance before transfer
|
||||
BEGIN BATCH
|
||||
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
|
||||
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
|
||||
APPLY BATCH;
|
||||
|
||||
-- Transaction can validate before transfer
|
||||
BEGIN TRANSACTION
|
||||
LET sender = (SELECT balance FROM accounts WHERE user_id = 1);
|
||||
|
||||
IF sender.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
|
||||
----
|
||||
|
||||
**Complex Business Logic:**
|
||||
|
||||
NOTE: Row reference arithmetic in SET clauses and comparing two row references are not currently supported. Pass values as parameters instead.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application code:
|
||||
-- double productPrice = 50.00; // Retrieved from products table
|
||||
|
||||
-- Transaction can enforce multi-step business rules
|
||||
BEGIN TRANSACTION
|
||||
LET user_account = (SELECT balance, status FROM accounts WHERE user_id = ?);
|
||||
LET product_info = (SELECT quantity FROM products WHERE id = ?);
|
||||
|
||||
IF user_account.status = 'active'
|
||||
AND user_account.balance >= ? -- Pass product_price as parameter
|
||||
AND product_info.quantity > 0 THEN
|
||||
|
||||
UPDATE accounts SET balance = balance - ? WHERE user_id = ?; -- Pass product_price
|
||||
UPDATE products SET quantity = quantity - 1 WHERE id = ?;
|
||||
INSERT INTO orders (id, user_id, product_id, amount) VALUES (?, ?, ?, ?); -- Pass product_price
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
**Error Prevention:**
|
||||
Transactions prevent invalid operations like:
|
||||
|
||||
* Overdrawing accounts
|
||||
* Selling out-of-stock items
|
||||
* Creating duplicate records
|
||||
* Violating business constraints
|
||||
|
||||
==== When to Continue Using BATCH
|
||||
|
||||
BATCH statements remain appropriate for:
|
||||
|
||||
* **Simple multi-table updates** without conditional logic
|
||||
* **Same-partition operations** where isolation is sufficient
|
||||
* **High-throughput scenarios** where transaction overhead isn't justified
|
||||
* **Counter updates** (transactions don't support counters)
|
||||
* **Backward compatibility** with existing applications
|
||||
|
||||
For detailed transaction syntax and examples, see the xref:developing/cql/transactions.adoc[Transactions] and xref:developing/cql/transactions-examples.adoc[Transaction Examples] documentation.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ For that reason, when used in this document, these terms (tables, rows and colum
|
|||
* xref:developing/cql/types.adoc[Data types]
|
||||
* xref:developing/cql/ddl.adoc[Data definition language]
|
||||
* xref:developing/cql/dml.adoc[Data manipulation language]
|
||||
* xref:developing/cql/transactions.adoc[Transactions]
|
||||
* xref:developing/cql/transactions-examples.adoc[Transaction patterns]
|
||||
* xref:developing/cql/transactions-migration.adoc[Transaction migration]
|
||||
* xref:developing/cql/dynamic-data-masking.adoc[Dynamic data masking]
|
||||
* xref:developing/cql/operators.adoc[Operators]
|
||||
* xref:developing/cql/indexing/indexing-concepts.adoc[Indexing]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,496 @@
|
|||
= Accord Transaction Design Patterns
|
||||
:page-nav-title: Transaction Patterns
|
||||
|
||||
This page provides common patterns and advanced design patterns for Accord transactions. These patterns solve common distributed system challenges that were difficult or impossible to address with eventual consistency.
|
||||
|
||||
For basic syntax and getting started, see xref:developing/cql/transactions.adoc[Accord Transactions]. For migration from LWT/BATCH, see xref:developing/cql/transactions-migration.adoc[Transaction Migration].
|
||||
|
||||
== Common Patterns
|
||||
|
||||
=== Read-Modify-Write
|
||||
|
||||
The fundamental transaction pattern: read current state, check a condition, then write. This covers conditional updates, conditional inserts, and any operation that must validate preconditions.
|
||||
|
||||
**Conditional update (e.g., sufficient balance):**
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET sender_account = (SELECT balance FROM accounts WHERE id = ?);
|
||||
|
||||
IF sender_account.balance >= ? THEN
|
||||
UPDATE accounts SET balance = balance - ? WHERE id = ?;
|
||||
UPDATE accounts SET balance = balance + ? WHERE id = ?;
|
||||
|
||||
INSERT INTO transactions (id, from_account, to_account, amount, timestamp)
|
||||
VALUES (?, ?, ?, ?, toTimestamp(now()));
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
**Conditional insert (e.g., prevent duplicates):**
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET existing_user = (SELECT user_id FROM email_index WHERE email = ? LIMIT 1);
|
||||
|
||||
IF existing_user IS NULL THEN
|
||||
INSERT INTO users (id, email, created_at, status)
|
||||
VALUES (?, ?, toTimestamp(now()), 'active');
|
||||
|
||||
INSERT INTO email_index (email, user_id) VALUES (?, ?);
|
||||
|
||||
INSERT INTO user_profiles (user_id, display_name)
|
||||
VALUES (?, ?);
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Multi-Table Updates
|
||||
|
||||
Maintain referential integrity across tables:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
UPDATE users SET status = 'suspended', suspended_at = toTimestamp(now())
|
||||
WHERE id = ?;
|
||||
|
||||
UPDATE orders SET status = 'cancelled'
|
||||
WHERE order_id = ?;
|
||||
|
||||
INSERT INTO audit_log (id, user_id, action, timestamp)
|
||||
VALUES (?, ?, 'user_suspended', toTimestamp(now()));
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Cross-Partition Transactions
|
||||
|
||||
Coordinate updates across different partitions (see xref:developing/cql/transactions.adoc#row-reference-limitations[row reference limitations] for parameter passing):
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT balance, status FROM users WHERE id = ?);
|
||||
|
||||
IF user_data.status = 'active' AND user_data.balance >= ? THEN
|
||||
UPDATE users SET balance = balance - ? WHERE id = ?;
|
||||
|
||||
INSERT INTO orders (id, user_id, total, status, created_at)
|
||||
VALUES (?, ?, ?, 'confirmed', toTimestamp(now()));
|
||||
|
||||
UPDATE inventory SET quantity = quantity - ? WHERE product_id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
== Pattern: Synchronous Unique Constraints
|
||||
|
||||
Cassandra's primary key enforces uniqueness, but what if you need uniqueness on a non-primary-key column like `email` or `username`? This pattern uses user-maintained lookup tables to enforce multiple unique constraints atomically.
|
||||
|
||||
=== The Challenge
|
||||
|
||||
You have a `users` table keyed by `user_id`, but you also need:
|
||||
|
||||
* Unique `email` addresses
|
||||
* Unique `username` values
|
||||
* The ability to **change** email or username while maintaining uniqueness
|
||||
|
||||
=== Schema
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE users (
|
||||
user_id uuid PRIMARY KEY,
|
||||
username text,
|
||||
email text,
|
||||
display_name text,
|
||||
created_at timestamp
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
-- Lookup tables for uniqueness enforcement
|
||||
CREATE TABLE username_index (
|
||||
username text PRIMARY KEY,
|
||||
user_id uuid
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
CREATE TABLE email_index (
|
||||
email text PRIMARY KEY,
|
||||
user_id uuid
|
||||
) WITH transactional_mode = 'full';
|
||||
----
|
||||
|
||||
=== Creating a User with Unique Constraints
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET existing_username = (SELECT user_id FROM username_index WHERE username = ? LIMIT 1);
|
||||
LET existing_email = (SELECT user_id FROM email_index WHERE email = ? LIMIT 1);
|
||||
|
||||
IF existing_username IS NULL AND existing_email IS NULL THEN
|
||||
INSERT INTO users (user_id, username, email, display_name, created_at)
|
||||
VALUES (?, ?, ?, ?, toTimestamp(now()));
|
||||
|
||||
INSERT INTO username_index (username, user_id) VALUES (?, ?);
|
||||
INSERT INTO email_index (email, user_id) VALUES (?, ?);
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Changing a Username
|
||||
|
||||
Renaming requires atomically: (1) verifying the new name is available, (2) deleting the old index entry, and (3) inserting the new one. Without transactions, a crash between steps could leave orphaned index entries or allow duplicates.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: user_id, old_username, new_username
|
||||
BEGIN TRANSACTION
|
||||
LET current_user = (SELECT username FROM users WHERE user_id = ?);
|
||||
LET new_name_owner = (SELECT user_id FROM username_index WHERE username = ? LIMIT 1);
|
||||
|
||||
-- Verify: user exists, old username matches, new username is available
|
||||
IF current_user IS NOT NULL
|
||||
AND current_user.username = ? -- old_username parameter
|
||||
AND new_name_owner IS NULL THEN
|
||||
|
||||
-- Update the user record
|
||||
UPDATE users SET username = ? WHERE user_id = ?; -- new_username
|
||||
|
||||
-- Atomically swap index entries
|
||||
DELETE FROM username_index WHERE username = ?; -- old_username
|
||||
INSERT INTO username_index (username, user_id) VALUES (?, ?); -- new_username, user_id
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
This pattern ensures the index tables are always consistent with the `users` table, even under concurrent modifications or partial failures.
|
||||
|
||||
== Pattern: Distributed State Machine
|
||||
|
||||
Many business objects follow a lifecycle with strict state transitions. An order might be `PENDING` -> `PAID` -> `SHIPPED` -> `DELIVERED`. Without transactions, concurrent operations (e.g., "cancel" and "ship") could both succeed, leaving the system in an invalid state.
|
||||
|
||||
=== The Challenge
|
||||
|
||||
* Ensure state transitions follow valid paths
|
||||
* Prevent race conditions between competing operations
|
||||
* Maintain audit trail of transitions
|
||||
|
||||
NOTE: The `IN` and `OR` operators are not currently supported in transaction `IF` conditions. To check multiple valid states, use **numeric status codes** with range comparisons (e.g., `status_code < 30` to mean "any state before SHIPPED").
|
||||
|
||||
=== Schema
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE orders (
|
||||
order_id uuid PRIMARY KEY,
|
||||
customer_id uuid,
|
||||
status_code int, -- 10=PENDING, 20=PAID, 30=SHIPPED, 40=DELIVERED, 99=CANCELLED
|
||||
status_name text,
|
||||
total_amount decimal,
|
||||
updated_at timestamp
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
CREATE TABLE order_status_history (
|
||||
order_id uuid,
|
||||
transition_time timestamp,
|
||||
from_status int,
|
||||
to_status int,
|
||||
actor_id uuid,
|
||||
PRIMARY KEY (order_id, transition_time)
|
||||
) WITH transactional_mode = 'full';
|
||||
----
|
||||
|
||||
=== Valid Transition: PAID -> SHIPPED
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: order_id, actor_id
|
||||
BEGIN TRANSACTION
|
||||
LET current_order = (SELECT status_code FROM orders WHERE order_id = ?);
|
||||
|
||||
-- Only allow transition from PAID (20) state
|
||||
IF current_order IS NOT NULL AND current_order.status_code = 20 THEN
|
||||
UPDATE orders
|
||||
SET status_code = 30, status_name = 'SHIPPED', updated_at = toTimestamp(now())
|
||||
WHERE order_id = ?;
|
||||
|
||||
INSERT INTO order_status_history (order_id, transition_time, from_status, to_status, actor_id)
|
||||
VALUES (?, toTimestamp(now()), 20, 30, ?);
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Handling Cancellation (Competing Transition)
|
||||
|
||||
Cancellation is only valid from certain states. If a "ship" and "cancel" operation race, exactly one will succeed.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: order_id, actor_id
|
||||
BEGIN TRANSACTION
|
||||
LET current_order = (SELECT status_code, customer_id, total_amount FROM orders WHERE order_id = ?);
|
||||
|
||||
-- Cancellation allowed only from PENDING (10) or PAID (20) states.
|
||||
-- Use range comparison (status_code <= 20) since IN/OR are not supported.
|
||||
IF current_order IS NOT NULL
|
||||
AND current_order.status_code <= 20 THEN
|
||||
|
||||
UPDATE orders
|
||||
SET status_code = 99, status_name = 'CANCELLED', updated_at = toTimestamp(now())
|
||||
WHERE order_id = ?;
|
||||
|
||||
INSERT INTO order_status_history (order_id, transition_time, from_status, to_status, actor_id)
|
||||
VALUES (?, toTimestamp(now()), current_order.status_code, 99, ?);
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
If a concurrent "ship" operation already moved the order to `SHIPPED` (30), the `status_code <= 20` condition fails and the cancellation is rejected. The application can check the result and inform the user.
|
||||
|
||||
== Pattern: Synchronous Denormalization
|
||||
|
||||
Cassandra best practices often involve denormalizing data for read performance. Keeping summary tables in sync with detail tables has traditionally been eventually consistent. Accord enables **synchronous denormalization** where aggregates are always accurate.
|
||||
|
||||
=== The Challenge
|
||||
|
||||
You have a `posts` table and want to maintain accurate counts per author without using counters (which aren't supported in transactions) and without eventual consistency lag.
|
||||
|
||||
=== Schema
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE posts (
|
||||
post_id uuid PRIMARY KEY,
|
||||
author_id uuid,
|
||||
title text,
|
||||
content text,
|
||||
status text, -- draft, published, archived
|
||||
created_at timestamp
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
CREATE TABLE author_stats (
|
||||
author_id uuid PRIMARY KEY,
|
||||
draft_count bigint,
|
||||
published_count bigint,
|
||||
archived_count bigint,
|
||||
last_post_at timestamp
|
||||
) WITH transactional_mode = 'full';
|
||||
----
|
||||
|
||||
=== Publishing a New Post (Increment Count)
|
||||
|
||||
This example demonstrates **idempotent creation**: if the application retries a failed publish request, the post won't be duplicated and the counter won't be incremented twice.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: post_id, author_id, title, content
|
||||
BEGIN TRANSACTION
|
||||
-- Check if this post already exists (idempotency guard)
|
||||
LET existing_post = (SELECT status FROM posts WHERE post_id = ?);
|
||||
|
||||
IF existing_post IS NULL THEN
|
||||
-- Create the post
|
||||
INSERT INTO posts (post_id, author_id, title, content, status, created_at)
|
||||
VALUES (?, ?, ?, ?, 'published', toTimestamp(now()));
|
||||
|
||||
-- Synchronously update the count exactly once
|
||||
UPDATE author_stats
|
||||
SET published_count = published_count + 1,
|
||||
last_post_at = toTimestamp(now())
|
||||
WHERE author_id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Changing Post Status (Transfer Between Counts)
|
||||
|
||||
When a post moves from `published` to `archived`, both counts must update atomically.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: post_id, author_id
|
||||
BEGIN TRANSACTION
|
||||
LET current_post = (SELECT status, author_id FROM posts WHERE post_id = ?);
|
||||
|
||||
IF current_post IS NOT NULL AND current_post.status = 'published' THEN
|
||||
-- Update post status
|
||||
UPDATE posts SET status = 'archived' WHERE post_id = ?;
|
||||
|
||||
-- Atomically transfer between counts
|
||||
UPDATE author_stats
|
||||
SET published_count = published_count - 1,
|
||||
archived_count = archived_count + 1
|
||||
WHERE author_id = ?; -- author_id passed as parameter
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Deleting a Post (Decrement Count)
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: post_id, author_id, current_status
|
||||
-- Application must query the post first to get status and author_id
|
||||
BEGIN TRANSACTION
|
||||
LET current_post = (SELECT status FROM posts WHERE post_id = ?);
|
||||
|
||||
IF current_post IS NOT NULL AND current_post.status = ? THEN -- current_status parameter
|
||||
-- Delete the post
|
||||
DELETE FROM posts WHERE post_id = ?;
|
||||
|
||||
-- Decrement the appropriate counter based on status
|
||||
-- Application passes which counter to decrement based on current_status
|
||||
UPDATE author_stats
|
||||
SET published_count = published_count - ? -- pass 1 if published, 0 otherwise
|
||||
WHERE author_id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
== Pattern: Multi-Entity Referential Integrity
|
||||
|
||||
Relational databases use foreign keys to prevent orphaned records. In Cassandra, you can achieve similar guarantees with transactions.
|
||||
|
||||
=== The Challenge
|
||||
|
||||
* A `Task` must belong to an existing `Project`
|
||||
* When a `Project` is deleted, handle its `Tasks` appropriately
|
||||
* Prevent creating tasks for non-existent projects
|
||||
|
||||
=== Schema
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE projects (
|
||||
project_id uuid PRIMARY KEY,
|
||||
name text,
|
||||
owner_id uuid,
|
||||
status text, -- active, completed, deleted
|
||||
task_count bigint,
|
||||
created_at timestamp
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
CREATE TABLE tasks (
|
||||
task_id uuid PRIMARY KEY,
|
||||
project_id uuid,
|
||||
title text,
|
||||
status text, -- open, in_progress, done
|
||||
assignee_id uuid,
|
||||
created_at timestamp
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
-- Index for finding tasks by project (for cleanup operations)
|
||||
CREATE TABLE tasks_by_project (
|
||||
project_id uuid,
|
||||
task_id uuid,
|
||||
title text,
|
||||
status text,
|
||||
PRIMARY KEY (project_id, task_id)
|
||||
) WITH transactional_mode = 'full';
|
||||
----
|
||||
|
||||
=== Creating a Task (Enforce Parent Exists)
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: task_id, project_id, title, assignee_id
|
||||
BEGIN TRANSACTION
|
||||
LET project = (SELECT status FROM projects WHERE project_id = ?);
|
||||
|
||||
-- Only create task if project exists and is active
|
||||
IF project IS NOT NULL AND project.status = 'active' THEN
|
||||
INSERT INTO tasks (task_id, project_id, title, status, assignee_id, created_at)
|
||||
VALUES (?, ?, ?, 'open', ?, toTimestamp(now()));
|
||||
|
||||
INSERT INTO tasks_by_project (project_id, task_id, title, status)
|
||||
VALUES (?, ?, ?, 'open');
|
||||
|
||||
UPDATE projects SET task_count = task_count + 1 WHERE project_id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Soft-Deleting a Project
|
||||
|
||||
Rather than cascading deletes (which would require iterating over all tasks), mark the project as deleted. Tasks can be cleaned up asynchronously or remain for audit purposes.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: project_id
|
||||
BEGIN TRANSACTION
|
||||
LET project = (SELECT status FROM projects WHERE project_id = ?);
|
||||
|
||||
IF project IS NOT NULL AND project.status = 'active' THEN
|
||||
UPDATE projects
|
||||
SET status = 'deleted'
|
||||
WHERE project_id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
Future task operations will fail the `project.status = 'active'` check, preventing modifications to a deleted project's tasks.
|
||||
|
||||
=== Moving a Task Between Projects
|
||||
|
||||
This pattern ensures both projects exist and are active, and maintains accurate task counts.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application provides: task_id, old_project_id, new_project_id, task_title, task_status
|
||||
BEGIN TRANSACTION
|
||||
LET task = (SELECT project_id, title, status FROM tasks WHERE task_id = ?);
|
||||
LET old_project = (SELECT status FROM projects WHERE project_id = ?); -- old_project_id
|
||||
LET new_project = (SELECT status FROM projects WHERE project_id = ?); -- new_project_id
|
||||
|
||||
IF task IS NOT NULL
|
||||
AND task.project_id = ? -- verify task belongs to old_project_id
|
||||
AND old_project.status = 'active'
|
||||
AND new_project.status = 'active' THEN
|
||||
|
||||
-- Update task's project reference
|
||||
UPDATE tasks SET project_id = ? WHERE task_id = ?; -- new_project_id
|
||||
|
||||
-- Update denormalized index: remove from old, add to new
|
||||
DELETE FROM tasks_by_project WHERE project_id = ? AND task_id = ?; -- old_project_id
|
||||
INSERT INTO tasks_by_project (project_id, task_id, title, status)
|
||||
VALUES (?, ?, ?, ?); -- new_project_id, task_id, task_title, task_status
|
||||
|
||||
-- Update counts on both projects
|
||||
UPDATE projects SET task_count = task_count - 1 WHERE project_id = ?; -- old_project_id
|
||||
UPDATE projects SET task_count = task_count + 1 WHERE project_id = ?; -- new_project_id
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
== Summary
|
||||
|
||||
These patterns demonstrate how Accord transactions solve problems that were previously difficult in Cassandra:
|
||||
|
||||
|===
|
||||
| Pattern | Problem Solved | Key Technique
|
||||
|
||||
| Synchronous Unique Constraints
|
||||
| Non-primary-key uniqueness
|
||||
| Lookup index tables with atomic swap
|
||||
|
||||
| Distributed State Machine
|
||||
| Race conditions in status changes
|
||||
| IF condition guards valid transitions
|
||||
|
||||
| Synchronous Denormalization
|
||||
| Stale aggregate counts, duplicate increments on retry
|
||||
| Idempotent creation with atomic detail + summary updates
|
||||
|
||||
| Multi-Entity Referential Integrity
|
||||
| Orphaned child records
|
||||
| Parent existence check before child operations
|
||||
|===
|
||||
|
||||
All patterns share common principles:
|
||||
|
||||
* **Read what you need**: Use LET to capture current state
|
||||
* **Guard with IF**: Validate preconditions before modifications
|
||||
* **Atomic updates**: All changes succeed or fail together
|
||||
* **Pass computed values as parameters**: Row-reference arithmetic in SET/VALUES is not supported
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
= Migrating to Accord Transactions
|
||||
:page-nav-title: Transaction Migration
|
||||
|
||||
This page covers migrating existing CQL patterns to Accord transactions, including LWT (Paxos), BATCH statements, and multi-statement workflows.
|
||||
|
||||
For transaction syntax and usage, see xref:developing/cql/transactions.adoc[Accord Transactions]. For design patterns, see xref:developing/cql/transactions-examples.adoc[Transaction Patterns].
|
||||
|
||||
== From Light Weight Transactions (LWT)
|
||||
|
||||
=== IF EXISTS
|
||||
|
||||
**Before (LWT):**
|
||||
[source,cql]
|
||||
----
|
||||
UPDATE accounts SET balance = balance - 100
|
||||
WHERE user_id = 12345
|
||||
IF EXISTS;
|
||||
----
|
||||
|
||||
**After (Accord):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET account_data = (SELECT balance FROM accounts WHERE user_id = 12345);
|
||||
|
||||
IF account_data IS NOT NULL THEN
|
||||
UPDATE accounts SET balance = balance - 100 WHERE user_id = 12345;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== IF NOT EXISTS
|
||||
|
||||
**Before (LWT):**
|
||||
[source,cql]
|
||||
----
|
||||
INSERT INTO users (id, email, status)
|
||||
VALUES (?, ?, 'active')
|
||||
IF NOT EXISTS;
|
||||
----
|
||||
|
||||
**After (Accord):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET existing_user = (SELECT id FROM users WHERE id = ? LIMIT 1);
|
||||
|
||||
IF existing_user IS NULL THEN
|
||||
INSERT INTO users (id, email, status) VALUES (?, ?, 'active');
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== IF column = null (NULL Comparison)
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
LWT and Accord handle `IF column = null` differently. See <<Null Handling>> for complete details.
|
||||
====
|
||||
|
||||
LWT treats `column = null` as "column is null," but Accord follows SQL semantics where `column = null` is always FALSE.
|
||||
|
||||
**Before (LWT):**
|
||||
[source,cql]
|
||||
----
|
||||
-- LWT: Matches when email is null/missing
|
||||
UPDATE users SET email = 'default@example.com'
|
||||
WHERE id = ?
|
||||
IF email = null;
|
||||
----
|
||||
|
||||
**After (Accord):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT email FROM users WHERE id = ?);
|
||||
|
||||
-- Must use IS NULL, not = null
|
||||
IF user_data IS NOT NULL AND user_data.email IS NULL THEN
|
||||
UPDATE users SET email = 'default@example.com' WHERE id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Complex CAS with Multiple Operations
|
||||
|
||||
**Before (Multiple LWT operations with race condition risk):**
|
||||
[source,cql]
|
||||
----
|
||||
UPDATE accounts SET balance = balance - 50
|
||||
WHERE user_id = 12345 AND balance >= 50
|
||||
IF balance >= 50;
|
||||
|
||||
-- Separate operation (not atomic with above)
|
||||
INSERT INTO transaction_log (id, user_id, amount, timestamp)
|
||||
VALUES (?, 12345, -50, toTimestamp(now()));
|
||||
----
|
||||
|
||||
**After (Single atomic transaction):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET account = (SELECT balance FROM accounts WHERE user_id = 12345);
|
||||
|
||||
IF account.balance >= 50 THEN
|
||||
UPDATE accounts SET balance = balance - 50 WHERE user_id = 12345;
|
||||
|
||||
INSERT INTO transaction_log (id, user_id, amount, timestamp)
|
||||
VALUES (?, 12345, -50, toTimestamp(now()));
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
== From BATCH Statements
|
||||
|
||||
Single-partition BATCH provides atomicity (all mutations apply or none do). Multi-partition (LOGGED) BATCH does not guarantee atomicity — it uses a batchlog for best-effort delivery. CAS can be used with BATCH for conditional checks, but only on a single partition, and if the condition fails the entire batch is rejected.
|
||||
|
||||
Accord transactions provide atomicity across multiple partitions with richer conditional logic:
|
||||
|
||||
**Before (Multi-partition BATCH - no atomicity guarantee, no condition checking):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN BATCH
|
||||
UPDATE users SET balance = 400 WHERE id = ?; -- Application pre-computes new balance
|
||||
INSERT INTO orders (id, user_id, amount) VALUES (?, ?, 100);
|
||||
APPLY BATCH;
|
||||
-- Problem: No atomicity across partitions, can't check if balance is sufficient!
|
||||
-- Application had to read balance separately, race condition possible.
|
||||
----
|
||||
|
||||
**After (Transaction with condition and cross-partition atomicity):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT balance FROM users WHERE id = ?);
|
||||
|
||||
IF user_data.balance >= 100 THEN
|
||||
UPDATE users SET balance = balance - 100 WHERE id = ?;
|
||||
INSERT INTO orders (id, user_id, amount) VALUES (?, ?, 100);
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
For single-partition operations that already use CAS BATCH, transactions provide equivalent guarantees with more flexible conditions:
|
||||
|
||||
**Before (CAS BATCH - single partition, single table only; condition fails = nothing applies):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN BATCH
|
||||
UPDATE users SET balance = balance - 100 WHERE id = ?
|
||||
IF balance >= 100;
|
||||
UPDATE users SET last_action = 'debit' WHERE id = ?;
|
||||
APPLY BATCH;
|
||||
----
|
||||
|
||||
**After (Transaction - same guarantees, works across partitions and tables):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT balance FROM users WHERE id = ?);
|
||||
|
||||
IF user_data.balance >= 100 THEN
|
||||
UPDATE users SET balance = balance - 100 WHERE id = ?;
|
||||
INSERT INTO user_history (user_id, action) VALUES (?, 'debit');
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
== Null Handling
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
Accord transactions follow SQL standard null semantics, which differ from LWT (Paxos) behavior. This is a critical difference when migrating from LWT to Accord.
|
||||
====
|
||||
|
||||
=== LWT vs Accord: NULL Comparison Behavior
|
||||
|
||||
|===
|
||||
| Condition | Column Value | LWT (Paxos) Result | Accord Result
|
||||
|
||||
| `IF column = null`
|
||||
| Column is NULL/missing
|
||||
| **TRUE** (matches)
|
||||
| **FALSE** (never matches)
|
||||
|
||||
| `IF column = null`
|
||||
| Column has a value
|
||||
| FALSE
|
||||
| FALSE
|
||||
|
||||
| `IF column IS NULL`
|
||||
| Column is NULL/missing
|
||||
| TRUE
|
||||
| TRUE
|
||||
|
||||
| `IF column != null`
|
||||
| Column is NULL/missing
|
||||
| FALSE
|
||||
| **FALSE** (null comparisons always false)
|
||||
|
||||
| `IF column != null`
|
||||
| Column has a value
|
||||
| TRUE
|
||||
| TRUE
|
||||
|===
|
||||
|
||||
=== Why the Difference?
|
||||
|
||||
**LWT (Paxos) behavior:** LWT treats `column = null` as a special case meaning "column is null or missing." This is a Cassandra-specific convenience that deviates from SQL standards.
|
||||
|
||||
**Accord behavior:** Accord follows SQL standard semantics where `NULL` represents an unknown value. Comparing anything to `NULL` using `=`, `!=`, `<`, `>`, etc. always returns `FALSE` (or more precisely, `UNKNOWN`, which is treated as `FALSE` in conditionals). This is because you cannot know if an unknown value equals another value.
|
||||
|
||||
=== Migration from LWT
|
||||
|
||||
If your LWT code uses `IF column = null`, you must update it when migrating to Accord transaction syntax.
|
||||
|
||||
NOTE: This does not affect LWT statements executed on Accord-enabled tables. Those statements retain their original LWT null semantics.
|
||||
|
||||
**Before (LWT):**
|
||||
[source,cql]
|
||||
----
|
||||
-- LWT: This returns TRUE when balance is null/missing
|
||||
UPDATE users SET balance = 100 WHERE id = ? IF balance = null;
|
||||
----
|
||||
|
||||
**After (Accord):**
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT balance FROM users WHERE id = ?);
|
||||
|
||||
-- Use IS NULL for null checks
|
||||
IF user_data IS NOT NULL AND user_data.balance IS NULL THEN
|
||||
UPDATE users SET balance = 100 WHERE id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
TIP: Null checks are recursive. If `user_data` is null (row doesn't exist), then `user_data.balance` is also null. So `IF user_data.balance IS NULL` will be true both when the row doesn't exist AND when the row exists but the column is null. Use `user_data IS NOT NULL` first only if you need to distinguish between these cases.
|
||||
|
||||
=== Common Null Patterns
|
||||
|
||||
**Check if a column is null (row missing OR column unset):**
|
||||
[source,cql]
|
||||
----
|
||||
IF user_data.balance IS NULL THEN
|
||||
-- Either row doesn't exist, OR row exists but balance is null
|
||||
END IF
|
||||
----
|
||||
|
||||
**Check if a row exists (regardless of column value):**
|
||||
[source,cql]
|
||||
----
|
||||
IF user_data IS NOT NULL THEN
|
||||
-- Row exists (balance could be null or have a value)
|
||||
END IF
|
||||
----
|
||||
|
||||
**Check if a column is null but row exists:**
|
||||
[source,cql]
|
||||
----
|
||||
IF user_data IS NOT NULL AND user_data.balance IS NULL THEN
|
||||
-- Row exists AND balance is null (distinguishes from missing row)
|
||||
END IF
|
||||
----
|
||||
|
||||
**Check if a column has a specific value:**
|
||||
[source,cql]
|
||||
----
|
||||
IF user_data IS NOT NULL AND user_data.balance = 100 THEN
|
||||
-- Row exists AND balance equals 100
|
||||
END IF
|
||||
----
|
||||
|
||||
**Check if a row does not exist:**
|
||||
[source,cql]
|
||||
----
|
||||
IF user_data IS NULL THEN
|
||||
-- Row does not exist
|
||||
END IF
|
||||
----
|
||||
|
||||
=== Null Propagation Rules
|
||||
|
||||
* Any comparison with null using `=`, `!=`, `<`, `>`, `<=`, `>=` returns **FALSE**
|
||||
* Only `IS NULL` and `IS NOT NULL` can meaningfully test for null
|
||||
* Arithmetic operations with null return null
|
||||
* Null columns in row references return null (not errors)
|
||||
* Accessing a field on a null row reference (e.g., `missing_row.column`) returns null
|
||||
|
|
@ -0,0 +1,500 @@
|
|||
= Accord Transactions
|
||||
:page-nav-title: Transactions
|
||||
|
||||
Accord provides strong consistency and ACID guarantees for Cassandra operations.
|
||||
When enabled on a table, **all CQL operations automatically execute through Accord** - no code changes required.
|
||||
For complex multi-step operations, explicit transaction syntax (`BEGIN TRANSACTION ... COMMIT TRANSACTION`) allows you to read, apply conditions, and write atomically across multiple partitions and tables.
|
||||
|
||||
== Overview
|
||||
|
||||
=== Key Benefits
|
||||
|
||||
* **Automatic Strong Consistency**: Normal CQL reads and writes become linearizable when `transactional_mode='full'`
|
||||
* **ACID Guarantees**: Atomicity, Consistency, Isolation, and Durability across multiple operations
|
||||
* **Multi-Partition Consistency**: Coordinate updates across different partition keys
|
||||
* **Multi-Table Support**: Update multiple tables atomically within a single transaction
|
||||
* **Complex Business Logic**: Support for conditional operations with multiple steps
|
||||
|
||||
=== When to Use Explicit Transactions
|
||||
|
||||
While normal CQL operations are automatically transactional with `transactional_mode='full'`, use explicit `BEGIN TRANSACTION ... COMMIT TRANSACTION` syntax when you need:
|
||||
|
||||
* **Read-Modify-Write Patterns**: Check a condition before making changes
|
||||
* **Complex Business Logic**: Multi-step operations that must be atomic
|
||||
* **Cross-Partition Operations**: Updates that span multiple partition keys
|
||||
* **Multi-Table Atomicity**: Ensure related changes across tables succeed or fail together
|
||||
|
||||
=== Safety & Consistency
|
||||
|
||||
Accord ensures data integrity through:
|
||||
|
||||
* **Strict Serializability**: Transactions execute as if in a single, total order that respects real-time ordering
|
||||
* **Conflict Detection**: Automatic handling of concurrent access to the same data
|
||||
* **Atomic Commitment**: All changes commit together or none at all
|
||||
* **Durable Writes**: Committed transactions survive node failures
|
||||
|
||||
== Getting Started
|
||||
|
||||
=== Prerequisites
|
||||
|
||||
Before using transactions:
|
||||
|
||||
. **Enable Accord globally** in `cassandra.yaml`:
|
||||
+
|
||||
[source,yaml]
|
||||
----
|
||||
accord:
|
||||
enabled: true
|
||||
----
|
||||
|
||||
. **Enable transactional mode on tables**:
|
||||
+
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
email text,
|
||||
balance decimal
|
||||
) WITH transactional_mode = 'full';
|
||||
----
|
||||
|
||||
See <<transactional-modes>> for detailed mode explanations.
|
||||
|
||||
=== Normal CQL Operations Are Transactional
|
||||
|
||||
When a table has `transactional_mode='full'`, your existing CQL statements are automatically executed through Accord. **You do not need to rewrite your application code.**
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- These normal CQL operations are automatically transactional:
|
||||
|
||||
-- Reads are executed through Accord
|
||||
SELECT id, email, balance FROM users WHERE id = 123e4567-e89b-12d3-a456-426614174000;
|
||||
|
||||
-- Writes are executed through Accord
|
||||
INSERT INTO users (id, email, balance) VALUES (123e4567-e89b-12d3-a456-426614174000, 'user@example.com', 100.00);
|
||||
|
||||
UPDATE users SET balance = 50.00 WHERE id = 123e4567-e89b-12d3-a456-426614174000;
|
||||
|
||||
DELETE FROM users WHERE id = 123e4567-e89b-12d3-a456-426614174000;
|
||||
----
|
||||
|
||||
Each statement executes as an individual Accord transaction, providing linearizability, consistency, and durability. Migrating to Accord can be as simple as enabling `transactional_mode='full'` on your tables. See <<transactional-modes>> for all available modes.
|
||||
|
||||
[[transactional-modes]]
|
||||
== Transactional Modes
|
||||
|
||||
Tables must be configured with one of these transactional modes:
|
||||
|
||||
=== transactional_mode='off' (Default)
|
||||
|
||||
* No Accord transaction support
|
||||
* Uses traditional Cassandra behavior
|
||||
* Lightweight transactions use Paxos protocol
|
||||
* Cannot participate in Accord transactions
|
||||
|
||||
**When to Use:**
|
||||
|
||||
* Tables not ready for transaction migration
|
||||
* High-throughput tables where transaction overhead isn't justified
|
||||
* Tables with existing Paxos-based logic that works well
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE tbl (...)
|
||||
WITH transactional_mode = 'off';
|
||||
----
|
||||
|
||||
[[transactional_mode_mixed_reads]]
|
||||
=== transactional_mode='mixed_reads'
|
||||
|
||||
NOTE: Most users should migrate directly from `off` to `full`. This mode is only needed for specific scenarios where you must mix transactional and non-transactional access on the same table during a transition period.
|
||||
|
||||
* Non-SERIAL writes are routed through Accord but committed at the supplied consistency level
|
||||
* Allows non-SERIAL reads to see transactionally written data
|
||||
* Blocking read repair is routed through Accord to avoid exposing uncommitted data
|
||||
|
||||
**When to Use:**
|
||||
|
||||
* Applications that **require** mixed transactional and non-transactional access on the same table simultaneously
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
* **Slower transactions**: Accord cannot perform single-replica read optimization because it must ensure data is readable at the non-SERIAL consistency level
|
||||
* **Read repair overhead**: Accord must repair stale replicas during reads
|
||||
* **Not recommended for most users**: Direct migration from `off` to `full` is simpler and provides better performance
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
ALTER TABLE tbl WITH transactional_mode = 'mixed_reads';
|
||||
----
|
||||
|
||||
=== transactional_mode='full' (Recommended)
|
||||
|
||||
* All reads and writes must occur through Accord transactions
|
||||
* Enables single-replica reads since non-transactional readers don't exist
|
||||
* Best transaction performance
|
||||
|
||||
**When to Use:**
|
||||
|
||||
* New tables that will use transactions
|
||||
* Tables migrating from `off` mode (recommended for most users)
|
||||
* Maximum transaction performance required
|
||||
|
||||
**Why this mode is faster:**
|
||||
|
||||
* **Single-replica reads**: Accord can read from a single replica and still provide correct results
|
||||
* **No read repair overhead**: Accord doesn't need to repair data for non-transactional readers
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
CREATE TABLE tbl (...)
|
||||
WITH transactional_mode = 'full';
|
||||
----
|
||||
|
||||
== Transaction Syntax
|
||||
|
||||
=== Basic Structure
|
||||
|
||||
All transactions follow this pattern:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
[LET assignments]
|
||||
[SELECT statements]
|
||||
[IF conditions THEN]
|
||||
[modification statements]
|
||||
[END IF]
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== LET Assignments
|
||||
|
||||
LET statements read data and bind it to variables for use later in the transaction:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT id, balance FROM users WHERE id = ?);
|
||||
LET account_data = (SELECT account_type FROM accounts WHERE user_id = ?);
|
||||
|
||||
IF user_data.balance > 100 AND account_data.account_type = 'premium' THEN
|
||||
UPDATE users SET balance = balance - 50 WHERE id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
**LET Requirements:**
|
||||
|
||||
* Each LET must specify a unique variable name
|
||||
* SELECT must return exactly one row (use `LIMIT 1` if needed)
|
||||
* All partition key columns must be specified with equality operators
|
||||
* Cannot use `ORDER BY`, `GROUP BY`, or aggregation functions
|
||||
* Cannot use range queries or multi-partition operations
|
||||
|
||||
**Valid LET Examples:**
|
||||
[source,cql]
|
||||
----
|
||||
LET user_data = (SELECT balance, status FROM users WHERE id = ?);
|
||||
LET order_info = (SELECT total, shipping_fee FROM orders WHERE id = ? LIMIT 1);
|
||||
LET static_config = (SELECT max_attempts FROM config WHERE setting_type = 'retry');
|
||||
----
|
||||
|
||||
**Invalid LET Examples:**
|
||||
[source,cql]
|
||||
----
|
||||
-- Missing LIMIT 1 with potential multiple results
|
||||
LET users = (SELECT * FROM users WHERE status = 'active');
|
||||
|
||||
-- Range query not allowed
|
||||
LET recent = (SELECT * FROM events WHERE id > ? AND id < ?);
|
||||
|
||||
-- Aggregation not supported
|
||||
LET total = (SELECT COUNT(*) FROM orders WHERE user_id = ?);
|
||||
----
|
||||
|
||||
=== Row References
|
||||
|
||||
Access fields from LET variables using dot notation:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET current_user = (SELECT balance, status FROM users WHERE id = ?);
|
||||
|
||||
-- Access fields with dot notation
|
||||
SELECT current_user.balance, current_user.status;
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
[[row-reference-limitations]]
|
||||
**Row Reference Limitations:**
|
||||
|
||||
Row reference arithmetic in SET clauses and row references in VALUES are not currently supported. Pass values as parameters instead.
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Application code computes values, passes as parameters
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT balance FROM users WHERE id = ?);
|
||||
|
||||
IF user_data.balance >= ? THEN -- Pass order_total as parameter
|
||||
UPDATE users SET balance = balance - ? WHERE id = ?; -- Pass order_total
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Returning Results
|
||||
|
||||
Return data from transactions using SELECT statements that appear before any modifications. You can use either row reference values or a normal single-partition SELECT.
|
||||
|
||||
NOTE: If no SELECT statement is present, the transaction returns an empty result set regardless of whether the IF condition was satisfied. Use a SELECT before modifications to observe transaction state.
|
||||
|
||||
==== Using Row References
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT balance, status FROM users WHERE id = ?);
|
||||
|
||||
-- Return row reference values (must come before UPDATE)
|
||||
SELECT user_data.balance, user_data.status;
|
||||
|
||||
UPDATE users SET balance = balance - 50 WHERE id = ?;
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
==== Using a Normal SELECT
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user_data = (SELECT balance FROM users WHERE id = ?);
|
||||
|
||||
-- Return data directly from table (must come before UPDATE)
|
||||
SELECT balance, status, email FROM users WHERE id = ?;
|
||||
|
||||
IF user_data.balance >= 50 THEN
|
||||
UPDATE users SET balance = balance - 50 WHERE id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
**Important:** SELECT statements must appear before any UPDATE, INSERT, or DELETE statements. To retrieve updated values, query outside the transaction after it commits.
|
||||
|
||||
=== Conditional Logic
|
||||
|
||||
Add conditional logic to transactions with IF blocks:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET sender = (SELECT balance FROM accounts WHERE user_id = ?);
|
||||
|
||||
IF sender.balance >= 100 THEN
|
||||
UPDATE accounts SET balance = balance - 100 WHERE user_id = ?;
|
||||
UPDATE accounts SET balance = balance + 100 WHERE user_id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
**Supported Operators:**
|
||||
|
||||
* Comparison: `=`, `<`, `<=`, `>`, `>=`, `!=`
|
||||
* Null checks: `IS NULL`, `IS NOT NULL`
|
||||
* Logical: `AND` only
|
||||
|
||||
**Complex Condition Examples:**
|
||||
[source,cql]
|
||||
----
|
||||
-- Multiple conditions with AND
|
||||
IF user_data.balance >= 100 AND user_data.status = 'active'
|
||||
AND user_data.credit_limit > 150 THEN
|
||||
-- statements
|
||||
END IF
|
||||
|
||||
-- Null checking
|
||||
IF account_info IS NOT NULL AND account_info.balance > 0 THEN
|
||||
-- statements
|
||||
END IF
|
||||
----
|
||||
|
||||
**Important Notes:**
|
||||
|
||||
* Null handling is strict (any null comparison returns false)
|
||||
* All modification statements must be inside the IF block when using conditions
|
||||
|
||||
== Restrictions
|
||||
|
||||
The following features cannot be used within `BEGIN TRANSACTION ... COMMIT TRANSACTION` blocks:
|
||||
|
||||
* **Counter tables** and counter operations
|
||||
* **Aggregation functions** (COUNT, SUM, AVG, etc.)
|
||||
* **ORDER BY** and **GROUP BY** clauses
|
||||
* **Custom TTL** and **timestamp** specifications (transaction manages timestamps)
|
||||
* **Range DELETE** operations (must specify complete primary key)
|
||||
* **Non-equality partition key** restrictions in LET/SELECT
|
||||
* **Individual statement conditions** (use transaction-level IF blocks instead)
|
||||
|
||||
=== Schema Requirements
|
||||
|
||||
* Accord must be enabled in `cassandra.yaml`
|
||||
* Tables must have a `transactional_mode` other than `'off'`
|
||||
* Tables being dropped cannot participate in transactions
|
||||
|
||||
== Syntax Reference
|
||||
|
||||
=== Complete Grammar
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
[LET letStatements]
|
||||
[SELECT selectStatement | SELECT rowDataReferences]
|
||||
[IF conditionalExpression THEN]
|
||||
[modificationStatements]
|
||||
[END IF]
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
=== Modification Statements
|
||||
|
||||
**INSERT:**
|
||||
[source,cql]
|
||||
----
|
||||
INSERT INTO table (columns) VALUES (values);
|
||||
----
|
||||
|
||||
**UPDATE:**
|
||||
[source,cql]
|
||||
----
|
||||
UPDATE table SET assignments WHERE conditions;
|
||||
----
|
||||
|
||||
**DELETE:**
|
||||
[source,cql]
|
||||
----
|
||||
DELETE [columns] FROM table WHERE conditions;
|
||||
----
|
||||
|
||||
== Multi-Partition Coordination
|
||||
|
||||
Cross-partition transactions add coordination overhead:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
LET user1 = (SELECT balance FROM accounts WHERE user_id = ?);
|
||||
LET user2 = (SELECT balance FROM accounts WHERE user_id = ?);
|
||||
|
||||
IF user1.balance >= 100 THEN
|
||||
UPDATE accounts SET balance = balance - 100 WHERE user_id = ?;
|
||||
UPDATE accounts SET balance = balance + 100 WHERE user_id = ?;
|
||||
END IF
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
**Performance Impact:**
|
||||
|
||||
* Same-partition transactions: Low overhead
|
||||
* Cross-partition transactions: Moderate overhead
|
||||
* More partitions = higher coordination cost
|
||||
|
||||
**Optimization:** Minimize cross-partition operations; consider data model changes to reduce cross-partition transactions.
|
||||
|
||||
== Automatic Read Generation
|
||||
|
||||
When UPDATE statements reference current column values, Accord automatically reads the current state:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
BEGIN TRANSACTION
|
||||
-- This UPDATE automatically reads current balance
|
||||
UPDATE users SET balance = balance + ? WHERE id = ?;
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
|
||||
**Triggers:**
|
||||
|
||||
* SET clauses that reference current column values
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
=== Configuration Errors
|
||||
|
||||
**Accord transactions are disabled**
|
||||
|
||||
Enable in cassandra.yaml:
|
||||
[source,yaml]
|
||||
----
|
||||
accord:
|
||||
enabled: true
|
||||
----
|
||||
|
||||
**Accord transactions are disabled on table**
|
||||
|
||||
Enable on table:
|
||||
[source,cql]
|
||||
----
|
||||
ALTER TABLE tbl WITH transactional_mode = 'full';
|
||||
----
|
||||
|
||||
=== Syntax Errors
|
||||
|
||||
**Duplicate variable names**
|
||||
|
||||
Use unique variable names:
|
||||
[source,cql]
|
||||
----
|
||||
-- Bad
|
||||
LET user_data = (SELECT balance FROM accounts WHERE user_id = 1);
|
||||
LET user_data = (SELECT balance FROM accounts WHERE user_id = 2);
|
||||
|
||||
-- Good
|
||||
LET sender_data = (SELECT balance FROM accounts WHERE user_id = 1);
|
||||
LET receiver_data = (SELECT balance FROM accounts WHERE user_id = 2);
|
||||
----
|
||||
|
||||
=== Performance Issues
|
||||
|
||||
**High latency investigation:**
|
||||
|
||||
. Check transaction complexity (LET count, partition count)footnote:[Keep LET statements under 5 and partitions under 3 as a starting point. These are suggested guidelines, not empirically derived limits.]
|
||||
. Monitor cross-datacenter operations
|
||||
. Check for high contention on specific keys
|
||||
|
||||
**Large result sets:**
|
||||
|
||||
Select only needed columns:
|
||||
[source,cql]
|
||||
----
|
||||
-- Bad: Reading entire row
|
||||
LET user_history = (SELECT * FROM large_history_table WHERE user_id = ? LIMIT 1);
|
||||
|
||||
-- Good: Only read necessary data
|
||||
LET user_status = (SELECT status, last_login FROM users WHERE id = ?);
|
||||
----
|
||||
|
||||
=== Counter Table Alternative
|
||||
|
||||
Counters cannot be used in transactions. Use regular columns:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
-- Bad: Counter in transaction
|
||||
CREATE TABLE page_views (
|
||||
page_id uuid PRIMARY KEY,
|
||||
views counter
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
-- Good: Regular column
|
||||
CREATE TABLE page_views (
|
||||
page_id uuid PRIMARY KEY,
|
||||
views bigint
|
||||
) WITH transactional_mode = 'full';
|
||||
|
||||
BEGIN TRANSACTION
|
||||
UPDATE page_views SET views = views + 1 WHERE page_id = ?;
|
||||
COMMIT TRANSACTION
|
||||
----
|
||||
Loading…
Reference in New Issue