TessariDB
Get started

Distribution

More than one machine, and one history between them.

A second node is two statements and a restart. What those statements buy is a cluster that agrees on who may write, moves the data by namespace and by shard rather than by machine, and refuses rather than guesses when it cannot be sure. What they cost is stated on this page beside what they buy, because a page that only lists advantages is a page nobody checked.

Three shapes

How it can be deployed

The same binary and the same statements produce all three. What separates them is what the catalog says, so moving between them is a declaration rather than a migration.

One node, and no peer declared
here:9000aloneeverything
there:9001not in this shape
elsewhere:9002not in this shape
What it buys
Nothing changes. A store whose catalog names nobody else writes the way it always has: it takes no lease, it is never fenced, and none of the machinery below is in its path.
What it costs
One machine is one machine. Losing it loses the service until it comes back, and the backup is the whole of the recovery story.
Four shapes, one engine and one set of statements — the difference between them is what the catalog says, not which build is running. A table split with SPLIT AT puts each shard on the nodes its subscriptions name, and a LEADS row gives a shard a leader of its own.

What you actually write

A cluster is declared, not configured

Peers are named in one transaction, because from the moment the first one commits this node is in a cluster and refuses local writes until a majority grants it a leadership — including the statements that would have described the rest of the cluster. One transaction is judged once, against a catalog that still names nobody.

Two peers, and what this machine is fortessariql
BEGIN;
DEFINE REPLICA second AT 'db-2.internal:9000'
    NODE '9f2c4e1a70bb43d5a1c6e2f480937d55'
    ROLES serving, coordinating;
DEFINE REPLICA third AT 'db-3.internal:9000'
    NODE 'c81b0f37a4e94a6f8d2e5417b90c3f26'
    ROLES serving, coordinating;
COMMIT;
DEFINE NODE ROLES serving, writable, coordinating;
the node answers
ok
ok
ok
ok
ok

The two halves are stored in two different places on purpose. What a machine is for is written to the machine; who its peers are is written to the catalog, and therefore travels in the backup. A node restored from another node’s backup comes up knowing every peer and nothing about itself, so it cannot inherit writable and start accepting writes the real writer never sees.

When a write counts

Acknowledged means a majority holds it

A write is readable on its leader the moment it commits. What the caller is told waits for more: on a namespace kept on more than one node, the answer comes once a majority of the range’s voters hold the write. Whichever majority elects the next leader includes one of them, so a leader lost a moment later takes nothing that was acknowledged with it.

clientleader · holds itfollower · holds itfollower · behind1 write2 copy3 okleader + one follower = 2 of 3 voters: a majority
At MAJORITY the caller hears ok once a majority of the range’s voters hold the write. Whichever majority elects the next leader includes one of them.

When a leader is lost

How long the cluster waits is one statement

A leader holds a lease and stops writing before it runs out, on elapsed time rather than the clock. Peers hear each other on the link between them, so a leader that goes quiet is noticed before the slower awareness round would find it; a node with the coordinating role then stands, and a majority of the voters elects it for the next epoch. The periods that decide all of this are a fact every node must agree on, so they are a replicated row rather than a file on each machine.

node aleads · epoch 4writesrefused: waitnode bleads · epoch 5a cut offa stops (fence)a's lease endsb elected
A leader cut off from the others stops at its fence, before the cluster may elect anyone else — so no instant has two writers. The gap between is what DEFINE FAILOVER sets.
The failover policy, for every node at oncetessariql
DEFINE FAILOVER
    AWARENESS 10s
    COLLECTION 10s
    ROUND 1s
    CAMPAIGN 1s
    LEASE 30s;
the node answers
ok

All five clauses are required, because they are checked against one another rather than one at a time. A set that would let a leader lose a leadership it could have kept, with no failure reported anywhere, is refused with the number to move named.

A campaign slower than the window it has to stand intessariql
DEFINE FAILOVER AWARENESS 10s COLLECTION 10s ROUND 1s CAMPAIGN 5s LEASE 30s;
the node answers
error: a failover policy's `campaign` is 5s against a `round` of 1s, and it
may not exceed 2s — twice the round time. The window between the moment a
leader should stand and the moment its fence shuts is exactly two round
times, so a slower cadence steps over it: the leader loses a leadership it
could have kept, and nothing reports a failure because no round was ever
attempted (at 0..75)

While nobody leads a range, that range takes no writes, and a write arriving there is told to wait rather than sent somewhere that is not yet true. The lease guard — the margin between a leader stopping and the cluster being allowed to replace it — is deliberately not settable.

One table, several machines

A table split by region, and changed while it serves

A table can name the field that decides where its records live. Each record is named after its region, so a region’s records sort together and SPLIT AT over region names gives each region a shard — a range with its own log, its own leader and its own followers. A read that fixes the region touches that one shard.

Split by region, and the one shard a read touchestessariql
DEFINE TABLE customers (region string, name string)
    IDENTITY uuid
    PARTITION BY region
    SPLIT AT 'de', 'fr';
CREATE customers = { region: 'de', name: 'Ada' };
EXPLAIN SELECT * FROM customers WHERE region = 'de';
the node answers
ok
'de:01a0f6a5-e55f-7540-9206-a2a2d8edaae9'
{ access: 'span', exact: true, shards: [2], table: 'customers' }

The map changes while the table takes writes. SPLIT AT retires the shard holding the point and mints two; MERGE SHARD joins two neighbours; ALTER REPLICA … LEADS hands a shard to another node. A new shard takes the next number the table has never used.

SPLIT AT 'g', 'p'123ALTER TABLE … SPLIT AT 'm'1453ALTER TABLE … MERGE SHARD 4, 5163
Each change retires the shards it replaces and mints new numbers. No record moves: a retired shard’s log keeps what was written to it.
A new region gets a shard of its owntessariql
ALTER TABLE customers SPLIT AT 'it';
CREATE customers = { region: 'it', name: 'Grace' };
EXPLAIN SELECT * FROM customers WHERE region = 'it';
the node answers
ok
'it:01a0f6a5-e560-7e5a-a3bc-6d895ccf5ebf'
{ access: 'span', exact: true, shards: [5], table: 'customers' }

No record moves when the map does: what a retired shard holds stays in its log and is still read, followed and backed up. And a table has to have been declared split for any of this to apply.

A table that was never splittessariql
ALTER TABLE ledger SPLIT AT 'm';
the node answers
error: table `ledger` is not split, so its records are filed in its database's log — declare a split table and move the records into it

A split table can keep its own shards within bounds. The store’s leader counts each shard every few seconds and splits one past the record count or write rate it was given, or merges two neighbours that fell under the floor — one change per table per pass, each the same statement an operator would have typed.

Shards kept within boundstessariql
ALTER TABLE customers SPLIT AUTOMATICALLY ABOVE 100000 RECORDS
    OR 500 WRITES PER SECOND
    MERGE BELOW 20000 RECORDS;
the node answers
ok
Bounds that would undo their own splittessariql
ALTER TABLE customers SPLIT AUTOMATICALLY ABOVE 1000 RECORDS MERGE BELOW 600 RECORDS;
the node answers
error: table `customers` would split above 1000 records and merge the halves back below 600 — the merge bound must be under half the split bound

A table named by generated identities can spread its new records over every shard instead of the newest one: SPREAD begins each identity with a bucket taken from its random part, so these three records land in three shards.

New records spread over the shardstessariql
DEFINE TABLE orders (total int) IDENTITY uuid SPREAD SPLIT AT '40', '80', 'c0';
CREATE orders = { total: 40 };
CREATE orders = { total: 41 };
CREATE orders = { total: 42 };
the node answers
ok
'c5:01a1030c-1801-7eeb-8d9c-f14937c5d2c5'
'7e:01a1030c-1802-78c4-9dfc-779b52f1097e'
'37:01a1030c-1802-7b29-9f8a-d3ec4cbcca37'

One transaction, two leaders

Whole or not at all, even across machines

When two nodes lead two ranges, a transaction writing both is refused unless it ends COMMIT ACROSS LEADERS. Then the node it ran on writes the transaction’s record on the first range’s leader, has every leader prepare its part as intents held by a majority, and turns the record committed only once every part is prepared — held by a majority before the caller hears it. A failure before that aborts it, and nothing applies anywhere.

clientthis noderange 1 leaderrange 2 leaderdrives itholds the recordcommit1 record PENDING2 prepare2 prepareevery part prepared3 COMMITTED4 committed5 resolve: intents become versions
Steps 1–3 are each held by a majority of their range’s voters before the next begins. The caller hears the outcome at 4; anything failing before 3 aborts it, and nothing applies.

A reader sees such a transaction whole or not at all. That is atomic visibility rather than one order across the cluster: there is no clock every node shares, so two readers on two ranges are not ordered against each other. A driver that dies part-way leaves a record that lapses and is aborted, so no intent is held for ever. The cost is two majority rounds and a round trip to each leader — worth paying where records must change together, and avoided by keeping related records in one leader’s range.

Who leads what

Placed, preferred, given back and evened out

A LEADS row places a range on a node. Several rows make several candidates, and PREFERRED names the one that should win: a leader that is not preferred hands the range over once the preferred node is caught up and audible. LEADS NONE on the last placement gives the range back to the store line — carefully, because the range’s own leader still holds a lease.

1 · placed2 · LEADS NONE3 · handed backstore line · a leadsshard 2 · b leadsstore line · a leadsshard 2 · b releasinga elected on itstore line · a leadsshard 2 includedwrites into shard 2 wait about one lease and an election
A range given back stays its own line until the store’s leader has been elected on it, then folds in: no instant has two writers.

Failovers leave leaderships wherever the elections put them. One clause on the failover policy asks the store’s leader to even them out: a node leading two lines more than an idle voter has its own placement moved there, at most once per two leases.

A failover policy that balances leadershipstessariql
DEFINE FAILOVER
    AWARENESS 10s
    COLLECTION 10s
    ROUND 1s
    CAMPAIGN 1s
    LEASE 30s
    BALANCE LEADERSHIPS;
the node answers
ok
beforeafternode aleads: store line, shard 2node cleads nothingbalancerone movenode aleads: store linenode cleads: shard 2
The balancer moves the busiest node’s own placement to an idle voter — the same two statements an operator would run, in one transaction.

Moving to a new release

One node at a time, writes throughout

Replace the followers first and the store’s leader last, each on its own store, waiting for each to answer before the next. One minor version at a time, forward only, and what the new release adds is turned on once the last node runs it.

startstep 1step 2step 3n3 · followern2 · followern1 · leaderoldnewnewnewoldoldnewnewoldoldoldnewturn on what the new release adds only after step 3
Followers first, the store’s leader last, one node at a time on its own store. Writes land after every step.

The refusals

It would rather refuse than be wrong

Three of them, each quoted as the node actually writes it. They are the argument of this page: a distributed store earns trust by what it declines to do when it cannot be certain, and the wording of a refusal is the part an operator reads at three in the morning.

A clustered node that holds no leadershiptessariql
CREATE ledger:1 = { amount: 1 };
the node answers
error: this node is in a cluster and holds no leadership: it does not
accept writes until a majority grants it one. A node that should be
leading needs the coordinating role (DEFINE NODE ROLES ... coordinating)
so that it stands for one

A cluster that has elected nobody takes no writes anywhere. That is a real availability change from a set of machines that each accepted writes the others would never see — and it is the point, because the second arrangement is faster and quietly wrong.

A namespace that does not say how many copies to keeptessariql
DEFINE NAMESPACE app;
the node answers
error: `app` (at 17..20) does not say how many copies to keep, and this
store declares 1 peer(s) that could hold one: write `REPLICATION NONE`
to keep a single copy deliberately, or `REPLICATION FACTOR <n>` to keep
more

On a single machine that statement is accepted, because there is nowhere for a second copy to go. Once a peer exists the same statement is a question nobody answered, and the engine keeps never stated as a different stored value from deliberately none rather than collapsing them into a default.

A read that asks for an answer fresher than the cluster can promisetessariql
SELECT * FROM ledger STALENESS 1s;
the node answers
error: a staleness bound of 1s (at 31..33) is tighter than this cluster
can know about itself: the floor is 20s

The bound is a routing instruction, not a label applied afterwards: it decides which nodes are allowed to answer. A bound the cluster cannot honour is refused at the API with the floor named, rather than accepted and quietly widened — which is the version that would have made the number meaningless.

What it buys

23 things that are true today

Each of these runs in the released engine. None of them is a direction, and none is on this page because it sounded good in a sentence.

Membership is a record, not a control plane

A node learns about its peers through the same log it replicates data with. There is no coordinator process, no configuration file to keep in step, and nothing to stand up beside the database.

Replication is per namespace, not per machine

A namespace declares how many copies the cluster keeps; a peer's subscription decides which of them it gets. A follower holding one tenant's namespaces cannot name another tenant's tables.

A read may say how stale an answer it accepts

The bound decides which nodes may answer rather than labelling the answer after the fact — and a bound tighter than the cluster can know about itself is refused with the floor named.

A misdirected write comes back as a redirect, not an error

It reaches the client as a frame carrying the endpoint, the node and the epoch it was decided under, with settled and transient told apart. Over HTTP it is a 307. Every client follows it — at most three hops, never back to an older leadership, the session's namespace selected again and the node checked on arrival.

A write sent to a follower reaches the leader

A node that may not write passes the write to the writable peer, at the address clients reach it on, and answers with what that peer answered.

A read a node cannot answer is sent where it can be

A node holding part of a split table sends a read it cannot gather — inside a transaction, or the read behind an UPDATE or DELETE — to a node it knows holds the whole table, instead of answering from its own part.

A leader stops writing before its lease expires

On monotonic elapsed time, not on the clock — so a node that has lost contact with the majority has already stopped by the time anyone else is entitled to start.

Losing a leader is noticed twice

A heartbeat on the peer link sees it before the slower awareness round does, and a healthy cluster stays quiet rather than chattering to prove it is alive.

A fork cannot be silent

The log position carries an epoch, the stream carries its predecessor's, so a follower refuses the first divergent record rather than a later one — and every refusal increments a counter that is published.

Peers prove themselves to each other

The link between nodes is mutually authenticated TLS, and a party presenting a client certificate cannot join as a peer, and an authenticated node with no grant is refused its subscription. A joining node is approved by its id, its certificate's fingerprint or a one-time token, a certificate is revoked with one statement every node obeys — ending even a link already open — and a removed node is never admitted again.

An acknowledged write survives its leader

A namespace kept on more than one node answers a write once a majority of its voters hold it. A write that could never reach a majority is refused before anything is written; one committed but not confirmed in time is refused saying that it is committed.

A node knows its region, and a write can wait for that region

A member row may name the region its node runs in — unset unless stated, changed or cleared with ALTER REPLICA. ACKNOWLEDGE LOCAL MAJORITY then answers once a majority of the voters in the leader's region hold a write: it survives losing every node outside the region while the leader lives, and is not promised across a failover, so MAJORITY stays the default.

Users travel with the data, inside its reach

A store-level user signs in on a selective follower; a namespace owner there cannot read another tenancy's credentials. The right to replicate is a store-level kind that a namespace-reach grant cannot carry.

A shard is led by its own node

A row naming a shard makes its node a candidate to lead that range alone, with its own election and lease, so writes to two shards are taken by two nodes at once.

One transaction may write two leaders' ranges

Asked for with COMMIT ACROSS LEADERS, it commits whole or not at all, in one round: the record and every leader's part go out together, and the caller is told committed once every part is held by a majority. A lost answer is settled by asking each leader whether its part landed, never by guessing. A reader sees it whole or not at all. Measured on one Linux host: 2.4 to 2.7 times one write. Unasked, it is refused, naming both nodes and the clause.

A table keeps its shards within bounds you state

SPLIT AUTOMATICALLY splits a shard past a record count or a write rate and merges neighbours that fall under a floor — each change the same SPLIT AT or MERGE SHARD an operator would type, through the log.

Leaderships are evened out when you ask

With BALANCE LEADERSHIPS the store's leader moves a placement from a node leading two lines more onto an idle voter, at most once per two leases, and never a preferred one.

A range goes back to the store line safely

LEADS NONE on its last placement waits out the range leader's lease before the store's leader takes it. Measured on three local processes with a writer running throughout: taken about 3.7 seconds after the release, and every write the writer was told was taken held exactly once.

A preferred node gets its range back

A leader that is not preferred hands its range to a preferred candidate that is caught up and audible — at most once every 30 seconds, so a node that keeps failing cannot have it thrown back and forth. Measured: the preferred candidate took the range about 2.6 seconds after it was named.

A cluster upgrades one node at a time

Followers first and the store's leader last, each on its own store: tested from 0.23.0-beta with writes landing after every step and every node holding every record at the end.

A read of the whole table is worked out where the data is

A node holding part of a split table gathers the rest from the shards' leaders, which apply the condition, rank an ordered top-n, and fold count, sum, mean, min, max, variance and stddev with their groups before sending — floats summed exactly, so the order the shards answer in changes nothing. A join side or a FETCH into a missing shard is gathered the same way. Otherwise the read is refused, never answered in part.

A shard's leader moves while it serves

Measured on three local processes with a writer running throughout: the move completed 11–20 seconds after it committed, and no write was taken for at most 6.2 seconds of it.

Two nodes can lead different namespaces

Each accepts writes for its own and redirects a write for the other's. The log sequence and the epoch are per range, not per store, which is what makes that possible at all.

What it does not do

The limits, said first

  • A table is split only if it was declared split. A table declared without SPLIT AT cannot be split later, and a split moves no record already written. The cluster splits and merges a split table’s shards only within bounds the table states, and it moves leaderships, never data: which nodes hold a range is what their subscriptions say.
  • Two writers on one range give up single-copy semantics, and that is not a tuning setting. Two writes that saw neither each other compare as concurrent — a third answer beside before and after — and the second is refused with both versions named. A table may instead declare LAST WRITER WINS, and where it does, the discarded write is counted and the counter is readable, because a write that vanishes without a number is the worst of the three outcomes.
  • A staleness bound has a floor, and the floor is how fast the cluster learns about itself. Asking for less is refused rather than rounded, so the number in your query is the number you get.
  • A transaction across leaders is atomic, not ordered. Readers see it whole or not at all, but no clock orders two readers on two ranges against each other, and write skew is admitted as it is under snapshot isolation. It costs several times a one-leader commit.