Transactions
What lands together, what does not land at all, and why a definition is inside the same boundary as a write.
Three words, and no options on any of them.
DEFINE NAMESPACE prod;
USE NAMESPACE prod;
DEFINE DATABASE orders;
USE DATABASE orders;
BEGIN;
DEFINE TABLE users (name string);
CREATE users:1 = { name: 'ada' };
COMMIT;
Both statements landed, and they landed together:
SELECT * FROM users;
One row. Note what was inside the boundary: a definition and a write.
There is no separate schema language here, so DEFINE TABLE is a statement like
any other and it commits like any other. A migration that creates a table and
fills it is one transaction, not a schema step followed by a data step with a
window between them where the table exists and is empty.
Cancelling leaves nothing
BEGIN;
DEFINE TABLE accounts (balance int);
CREATE accounts = { balance: 10 };
CANCEL;
Not the record, and not the table either:
SELECT * FROM accounts;
Refused — Unknown. The table was never there. A store that rolled back the row
and kept the table would leave the schema and the data disagreeing about what
happened, and the disagreement would survive the session that caused it.
Trying a write without making it
COMMIT checks and keeps. CANCEL discards without checking — every check
that refuses a write runs inside the commit, so until there was a third word the
only way to find out whether a write would be refused was to send it.
VERIFY is that third word. It runs every check a COMMIT runs, then throws the
work away:
BEGIN;
DEFINE TABLE notes (body string);
INSERT INTO notes (body) VALUES ('here'), (42);
VERIFY;
Refused, in exactly the words the real write would have used. Nothing was written, and the table is not there either:
SELECT * FROM notes;
Refused — Unknown.
A rehearsal that passes writes nothing at all:
BEGIN;
DEFINE COLLECTION drafts;
CREATE drafts = { body: 'here' };
VERIFY;
SELECT * FROM drafts;
Refused — Unknown again, which is the point: the checks all passed and the
work was still discarded.
It is not a second validation path. In the engine VERIFY is the commit with its
last step — writing the batch — left out, which is one line of difference. That
matters because a rehearsal reaches every check the write does, including the
ones that live past the schema: a unique-index collision is raised while the
index entries are being maintained, and a separate validator would have missed
it and told you the write was fine.
VERIFY needs an open transaction, exactly as COMMIT and CANCEL do —
NoOpenTransaction otherwise.
A transaction nobody closed is discarded
BEGIN;
CREATE users:2 = { name: 'grace' };
UnclosedTransaction at the end of the input. The write is not there:
SELECT * FROM users;
Still one row. This is the case worth being deliberate about, because the two possible behaviours are both defensible and only one is safe: a store that committed on disconnect would turn a client crash into a partial write, and one that stayed open would hold locks for a session nobody is in. It discards, and it says so rather than discarding quietly — silence here looks exactly like a transaction that ran.
The two refusals
BEGIN;
BEGIN;
NestedTransaction. There is one boundary, not a stack of them. Nested
transactions mean savepoints, savepoints mean partial rollback, and partial
rollback means a failed inner block leaves the outer one in a state the author
has to reason about.
COMMIT;
NoOpenTransaction. A commit with nothing open is a mistake in the script,
usually a BEGIN that was lost to an editor, and treating it as a no-op would
let the statements it was supposed to protect run unprotected.
The unit is the database
A transaction spans everything in the selected database, including the parts of it that are not tables:
BEGIN;
DEFINE SPACE sessions;
SET sessions:'abc' = users:1;
CREATE users:3 = { name: 'hopper' };
COMMIT;
SELECT * FROM users;
Two rows — users:1 and users:3, since users:2 was the one discarded above.
The key-value space and the table committed together,
because they are in one database and the database is the boundary. A session
token written beside the record it refers to either both exist or neither does.
On a cluster a transaction is committed by the node that leads what it
writes. One writing ranges that two different nodes lead is refused
SpansLeaderships unless it ends COMMIT ACROSS LEADERS, which commits it on
both, whole or not at all — see
Transactions across leaders.
What is not here
There is no isolation level to choose and no read-your-own-writes flag. A
statement outside BEGIN is its own transaction, which is the behaviour of
every statement you have run so far without thinking about it.
Contention is reported rather than resolved by waiting: a commit that cannot be
serialised against another is refused as CommitContention, and retrying is the
caller's decision because only the caller knows whether the work is still worth
doing.