TessariDBDocs
  • 0.34.2-betalatest
  • 0.34.1-beta
  • 0.34.0-beta
  • 0.33.4-beta
  • 0.33.3-beta
  • 0.33.2-beta
  • 0.33.1-beta
  • 0.33.0-beta
  • 0.32.0-beta

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.

Transactions — TessariDB