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

The command line

Every argument the binary takes, how a script is run from a file or a pipe, and the one thing worth knowing before you run a migration: a script is not a transaction.

The binary is given one store and one thing to do with it. Everything below follows from that sentence, including most of what it refuses.

Everything it takes

This is the binary's own usage, with the column a usage line has no room for.

writtenshortwhat it doeswhere it is explained
<path>a store on disk — omitted, the store is in memory and is lost when the process endsYour first store
--at <host:port>a running node, instead of a store in this processConnecting
--user <name>sign in as this user; the password comes from the environmentbelow
--serve <host:port>serve this store over the wire protocol until stoppedServing
--http <host:port>serve it over HTTP; may accompany --serve, and one process then holds bothServing
--param <name>=<value>bind $name for the script that follows; repeatablebelow
--execute <script>-erun this and exitbelow
--file <path>-frun this file and exitbelow
--backup <file>write the store's current state (a snapshot) to a file and exit; with --from, its log instead; with --at, a running node's snapshot, streamed (from 0.34.0-beta)Backup and restore
--snapshot <file>write the store's current state to a file and exitBackup and restore
--dump <file>write the store's current state as TessariQL to a file and exitBackup and restore
--restore <file>replay a file into an empty store and exitBackup and restore
--verify <file>read a backup and say what it holds, changing nothingBackup and restore
--from <n>with --backup: write the log from sequence n (1 for the whole log)Backup and restore
--upto <n>with --restore: stop replaying after sequence nBackup and restore
--backup-dir <folder>where a serving node writes BACKUP … TO '<name>'; without it every TO is refusedBackup and restore
--unseal-for <duration>how long an unseal lasts before the store seals itself (10m, 1h); default TESSARIDB_UNSEAL_FOR or 10mVaults
--tls-cert <file>serve clients over TLS 1.3 with this certificate chain, PEM; default TESSARIDB_TLS_CERTEncryption in transit
--tls-key <file>its private key, PEM, readable by its owner alone (mode 600, or 640 when root owns it; refused otherwise); default TESSARIDB_TLS_KEYEncryption in transit
--require-client-tlsrefuse to start unless --tls-cert and --tls-key are given, for a deployment that forbids the clear; or TESSARIDB_REQUIRE_CLIENT_TLS=1; off by defaultEncryption in transit
--client-plaintextretired: the clear is the default without a certificate; refused from 0.24.0-betaEncryption in transit
--tls-authority <file>with --at: speak TLS and trust the node by these certificates, PEM; default TESSARIDB_TLS_AUTHORITYEncryption in transit
--encryption-key-file <file>encrypt the store's files and seal every backup it writes under the 32 bytes in the file (mode 600); default TESSARIDB_ENCRYPTION_KEY_FILEEncryption at rest
--backup-key-file <file>with --restore or --verify: the key a sealed backup was sealed under, when it is not the store'sEncryption at rest
--healthsay whether the store is well, and exit non-zero if it is notServing
--version-Vsay which build this is, and exitInstall
--help-hprint the table above, without the last columnit is the table above
--cluster-credential <file>this node's peer credential, PEM, with --serveThe cluster
--cluster-key <file>its private key, PEM, readable by its owner alone (mode 600, or 640 when root owns it; refused otherwise)The cluster
--cluster-authority <file>the one certificate this cluster trusts, PEMThe cluster
--cluster-address <host:port>where this node's own peer door bindsThe cluster
--seed <node-id>@<host:port>a node to reach the cluster through; repeatableThe cluster
--join-token <token>a token from CREATE JOIN TOKEN, offered to the seeds until a row names this node; default TESSARIDB_JOIN_TOKENAdding a node

Two of these need no store at all — --version and --verify — and that is deliberate. The first thing anyone does with a binary they have just been handed is ask what it is, and a backup that can only be checked by restoring it is a backup nobody checks.

An unknown option is an error, not something ignored:

$ tessaridb --nonsense
unknown option "--nonsense"

A session opened with a misspelled flag that quietly used the default is how somebody writes to the wrong store. The same reasoning produces the refusals further down.

Running a script

Three ways in, and the store cannot tell them apart:

$ tessaridb ./data -e 'USE NAMESPACE prod; USE DATABASE orders; SELECT name FROM users;'
$ tessaridb ./data -f migrate.tessariql
$ cat migrate.tessariql | tessaridb ./data

.tessariql is the conventional extension. Nothing reads it — -f takes any path. The same three, worked through from an empty store, are Run a file instead of typing.

With neither -e nor -f, statements are read from standard input: a prompt when that is a terminal, a script when it is a pipe. The pipe gets no prompt and no greeting, so what comes back is the script's output rather than a transcript of a session:

$ tessaridb ./data -f first.tessariql
ok
ok
ok
ok
ok
ok
1: { name: 'ada' }
(1 record(s), via scan)

One ok per statement that answered nothing. A statement that answers with records prints them, then how many there were and how they were reached.

A script is not a transaction

This is the part worth reading before a migration.

A script runs statement by statement and stops at the first refusal, with exit 1. What ran before the refusal has already happened, and nothing undoes it. The boundary is not the file — it is wherever you put BEGIN.

a file CREATE users:1 kept CREATE users:1 refused CREATE users never ran the same file, in a transaction BEGIN CREATE users:1 CREATE users:1 COMMIT — nothing lands
The same refusal, on either side of a transaction boundary.

Four scripts, each run against its own empty store on disk, and each store read back afterwards:

the scriptthe runwhat the store held after
a file, and a statement in the middle is refusedstops there, exit 1everything before it — kept; everything after it never ran
a file with a syntax error on the last linethe earlier lines ran, exit 1everything before it — kept
the same refusal inside BEGIN … COMMITthe transaction aborts, exit 1nothing
a one-line -e with a syntax error anywhere in itnothing ran at allnothing — not even the DEFINE NAMESPACE in front of it

The last row differs from the second because of where the statements were cut. A file is read a statement at a time, so the lines before the bad one have already gone to the store. A whole -e string on one line is submitted as one piece, so it is parsed before any of it runs, and one bad token anywhere means none of it does. Both are defensible; they are not the same, and a migration that assumes one and gets the other lands halfway.

So the way to say all of it or none of it is to say so:

DEFINE COLLECTION staff;
BEGIN;
CREATE staff = { name: 'ada' };
CREATE staff = { name: 'grace' };
COMMIT;
SELECT name FROM staff;

A transaction spanning several lines of a file is held together as one — the ; after BEGIN does not end it. And a statement outside BEGIN is its own transaction, which is why the first row of that table leaves a record behind rather than nothing: see Transactions.

The password is not an argument

There is no --password, and that is the feature:

$ tessaridb --at 127.0.0.1:7654 --user editor --password hunter2
unknown option "--password"

An argument is readable by anyone who can list processes on the machine, and it outlives the session in the shell history. The password is read from TESSARIDB_PASSWORD instead, and --user says so when it is missing:

$ tessaridb --at 127.0.0.1:7654 --user editor
tessaridb: --user needs the password in TESSARIDB_PASSWORD, and it is not set

Who the users are and what each may do is Users.

Values that come from outside the script

--param binds one parameter, and repeats:

$ tessaridb ./data --param "who='ada'" --param 'limit=10' -f report.tessariql

The value is written as TessariQL, not JSON, so dec 12.34, 2s and datetime '…' all say what they are — and what an answer prints pastes straight back into the next parameter. It is read in isolation, as a value and nothing else:

$ tessaridb --param 'x=1; DROP TABLE users' -e 'SELECT * FROM users;'
--param x: "1; DROP TABLE users" is not a value TessariQL can read on its own

That reader is shared with the parameters field of an HTTP request body, so the two surfaces cannot drift into reading a supplied value differently. Why nothing a caller supplies can become syntax — bound after the statement is read, rather than escaped into it — is Parameters.

What a running node reports

TESSARIDB_LOG sets how much a node says on standard error — one line per event, with a UTC timestamp, the level, the source, the spans it happened inside and a sentence, followed by its values as named fields:

2026-10-04T14:32:45.138963Z  INFO tessaridb::serving::announcing: serving the wire protocol address=127.0.0.1:9080
2026-10-04T14:33:02.511207Z  WARN connection{connection=7}: tessari_session::session::signing: sign-in refused user=ada

A value is never spliced into the sentence, so user=ada or connection=7 can be searched for, and every line a connection produced carries its number.

valuewhat you get
offnothing
errorrefusals the node could not handle
warnthose, and what it recovered from
infothe default — and connections accepted, sign-ins refused
debugthose, and what the node did internally
traceeverything

info is the default rather than warn because a node that reports only its problems cannot answer was it even reached — which is the first question when a client sees nothing.

Besides one level, TESSARIDB_LOG takes per-module directives — info,tessari_wire=debug reports the wire protocol in detail and everything else at info. A value it cannot read is reported once and info is used.

TESSARIDB_LOG_FORMAT chooses the shape: text (the default) for a person, or json for a collector — one object per line, the event's fields flattened beside timestamp, level, target and message, and the spans it happened in under spans. Colour is used only when standard error is a terminal and NO_COLOR is not set.

A value it cannot read is not an error. It reports at the default and starts normally; refusing to open a database because a log level was misspelled would be the worse of the two outcomes.

The node's own timers

What a node does on a timer — housekeeping, the cluster's greeting, collection, campaign, settling and balancing rounds, the credential watch — runs one pass at a time, and every line a pass produces carries cadence{cadence=…}. Each pass ends one of three ways: it completes (said at debug, because it happens every period), it fails — pass failed, with what it could not do and why — or it overruns its period — pass overran, said once; the next pass waits for it rather than running beside it. A failed pass does not stop the timer.

2026-10-05T03:12:09.402117Z  WARN cadence{cadence="collection"}: tessari_wire::driver::cadence: pass failed elapsed_ms=3 error=the collection pass failed: this node cannot say who its peers are: …

Arguments that are refused together

Each of these could have been resolved by ignoring something. None of them is, because a value silently dropped is a value somebody believes was used — and here what they believe is that a port is open, or that a backup was bounded.

askedanswered
/tmp/a /tmp/bonly one store may be opened, and "/tmp/b" is a second
--at … --backup f --from 5--from works on a store this process opened, and --at names one it did not
--param x=1 --backup f--param binds a value in a script, and --backup runs none
--from 5 -e '…'--from bounds a --backup and --upto bounds a --restore
--serve … --healthan address to serve on and something else to do are two programs

The five cluster options are given together or not at all. Told some of them, a node refuses to start rather than serving with credentials nobody checked; told none of them, it is the single node it is today. There is no default peer address, because there is no port this engine claims and a defaulted listener is a door the operator did not know they opened.

--version and --help are the exception: they name this binary rather than the node at --at, so an address alongside them is neither refused nor consulted.

Exit codes

codemeans
0it did what was asked
1it did not, and the reason was printed

Two of them are meant to be read by a script rather than a person. --health exits non-zero when the store is unwell, so a cron line needs no output parsing:

$ tessaridb ./data --health
well — committed to sequence 0

And --verify exits non-zero on a backup that was cut short, which is the condition you are verifying for — the warning is on the error stream, so a script can act on the status without reading either:

$ tessaridb --verify ./monday.tessarilog
0 record(s), sequences 1..=5, good through 0
warning: ./monday.tessarilog was cut short — it says it holds through 5 and reads through 0
$ echo $?
1

Where in a backup routine to put that call — and why it is before the file is moved into place — is Checking one.

The command line — TessariDB