pro/sql and key-value database/0.5, source not yet published
TRIC+ A high-performance SQL and key-value database You choose how long. It chooses where.
TRIC+ is an embedded database you can address in SQL or by key, and it works out for itself where each value should live. Speak to it either way: the same data, the same engine, and no bridge between two systems to keep in step. Where a value belongs is not something you configure. Hand it a schema and an analyser derives the storage plan; write a value with a lifetime and that lifetime decides its tier. What follows is one argument in seven steps, and the figures were taken on a machine that is named.
Runs on
- FreeBSD
- Linux
- macOS
Built with
- Rust
- SQLite
Bridges ready
- C
- C++
- JavaScript
- Lua
- Python
- Swift
- Tcl
- Kind
- SQL and key-value database, as a library and a server
- Architecture
- Supervised modules over a pluggable data bus
- Analyser
- An SQL schema becomes a storage plan, by seven rules
- Host
- Rust
- Transient tier
- A
BTreeMapin memory - Persistent tier
- SQLite, WAL, one file per namespace
- Routing
- A time to live sends it to memory, its absence to disk
- Primitives
- Six: read, write, delete, compare-and-delete, TTL, prefix scan
- Wire
- Unix datagram locally, UDP over a network
- On the wire
- ChaCha20-Poly1305, padded so the shape says nothing
- SQL dialect
- A subset via
sqlparser: MySQL, PostgreSQL and SQLite syntax all read - Authentication
- ed25519 and X25519 handshake, sessions, authorised keys
- Deploy
- One binary:
tric server, andtricis the CLI - Bridges
- Ten ready today, of twenty planned
- Licence
- Business Source, Apache-2.0 four years on
Startbefore the argument
Before the argument, the facts
Version 0.5, and the source is not yet published. The benchmarks below were taken on FreeBSD 15 on an AMD Ryzen 5 3600 with ZFS, inside a jail, which is the deployment target this is built for and tuned on; they will read differently on other hardware and the page says which machine rather than leaving you to assume yours. Ten of twenty planned language bridges work today. The licence is the only one in this house with a delay in it, and it gets a chapter rather than a badge.
The argument, in seven steps
- OneIt works out where things belongA schema becomes a storage plan, and a lifetime picks a tier.
- TwoSQL and keys, one engineSix primitives underneath, and SQL that maps onto them rather than beside them.
- ThreeTwo tiers, one engineA map in memory and SQLite on disk, with hot keys promoted back.
- FourWhat crosses the wireEncrypted per datagram and padded, so an observer learns neither size nor verb.
- FiveMeasured, on a named machineThree layers, one payload, one method, and the machine written down.
- SixWhere it is not closeTwo operations where the difference is a factor rather than a percentage.
- SevenOne binary, and the bridgesTen languages today, C the foundation the rest of them stand on.
Then, for reference
01one decision, not two systems
Placement is a decision it makes, not one it asks you for
A cache and a database, two systems to install, two to configure, two to keep in step, and a developer in the middle deciding for every value which one it belongs in. The decision is nearly always the same one, made again and again: does this need to survive a restart, or not.
TRIC+ asks it once, in the only place where the answer is already known. You state how long the value should live. Everything else follows.
tric.write(b"session:9f2", token, Some(Duration::from_secs(900))); // memory
tric.write(b"user:4711", record, None); // disk
That is the whole of it. A time to live places the value in the transient tier, a
BTreeMap in memory. Its absence places the value in the persistent tier,
SQLite on disk. The call site does not name a tier, does not open a second connection and
does not consult a configuration file, because there is no knob to turn: the routing is a
property of what you said, not of how the system was set up.
The name for it is permutive storage, which sounds grander than the idea deserves. The idea is that a lifetime already implies a home, and that asking the developer to state both is asking twice.
And the same again, one level up
The write-time rule is the small version. Hand the server an ordinary SQL schema and an analyser reads it and derives a storage plan from it: the tables, their columns and types, which columns are the primary key, which are foreign keys into other tables, and, among all of them, which column is a lifetime. Seven deterministic rules, the same schema always giving the same plan.
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id),
payload BLOB,
ttl INTEGER -- the analyser finds this by itself
);
So the layout is not something you design against the store's two tiers, and it is not something a configuration file describes. It falls out of the schema you would have written anyway. That is the whole of what is meant by a database that decides where things belong: not a heuristic watching traffic and moving data about behind your back, but a plan derived from what you said the data is, deterministically, before a byte is written.
Which leaves the question of what you actually say to it.
02six verbs and no more
Six primitives, and SQL that lands on them
The whole surface, and it is deliberately short. A store that grows a verb for every pattern its users invent ends up with three hundred of them and a manual nobody finishes.
The ordinary three, with the lifetime on the write deciding the tier
Atomic on the server, so a lock does not have to be built on top of it
Set, read and changed after the fact, not only at write time
A first-class operation rather than a full sweep wearing a filter
SQL is not a second system beside these. The parser is sqlparser with the
generic dialect, so MySQL, PostgreSQL and SQLite syntax all read without switching modes,
and each statement lands on the same six verbs: WHERE key = '42' becomes a
read, WHERE key LIKE 'admin-%' becomes a prefix scan, and a query with no
WHERE at all scans the table. There is no second query engine to keep in step,
because there is no second engine.
The last two verbs are the ones that matter in practice and the ones most stores make you improvise. Compare-and-delete is how a lease is released safely by whoever holds it and nobody else; done in a script on the client it is a race with better manners. Prefix scan is how a namespace is enumerated; done by listing every key and filtering it is a habit that works beautifully until the day the store is large.
Six verbs over two tiers. What actually happens underneath them?
03two tiers, one engine
A map, a database, and the traffic between them
The transient tier is a BTreeMap, which is to say memory with an ordering,
which is what makes prefix scan cheap rather than clever. The persistent tier is SQLite in
WAL mode, one database file per namespace, so concurrent readers do not queue behind a
writer.
They are not two systems with a wrapper around them. A read of a persistent key that is being asked for often promotes that key into the transient tier for a minute, so the second caller pays memory prices for something the first paid disk prices for. Nothing about that is visible at the call site, and nothing about it needs configuring.
Two properties of the arrangement are easy to miss and are the reason it stays honest. A key has one home and never two: writing without a lifetime drops whatever copy memory was holding, and writing with one drops whatever copy the disk was holding, so there is no second answer anywhere to go stale. And attaching a lifetime to something already on disk does not copy it into memory, it moves it, which is what makes a lifetime a statement about the value rather than a hint to a cache.
Where a value is put
How a value is found
WHERE decides
what actually happens. An equality is one key read, a LIKE with a
prefix is an ordered scan, and no condition at all is the whole table. Then the same
two tiers in order: the map answers if it can, and a value found on disk is copied
up for a minute on its way out, so the second caller pays memory prices for what the
first paid disk prices for.Instances live under /var/db/tric/, one directory per project, each with
numbered slots so a staging copy or a migration rehearsal is a clone rather than a second
installation. Data goes in and out as native SQL dumps for MySQL, PostgreSQL and SQLite, or
as a Brotli-compressed .tric archive, and an import can be differential: it
applies what changed rather than everything.
All of which is on one machine. What happens when the caller is on another?
04what crosses the wire
The shape of a request says more than people think
Locally the protocol runs over a Unix datagram socket, which never leaves the machine. Across a network it runs over UDP, and there each datagram is encrypted with ChaCha20-Poly1305 and padded with random noise.
The padding is the part worth pausing on, because encryption alone would not have been enough. An observer who cannot read your traffic can still count it, and the length of a request is a surprisingly talkative thing: reads and writes have different shapes, a large value looks different from a small one, and a burst of one operation type at a known hour tells a story. Padded to a uniform shape, the wire carries the same silhouette whatever it is carrying, and the observer is left with the fact that something happened.
There is also a SQL door into the persistent tier, tric query, which reads
the SQLite side directly for the times when what you want is a question rather than a key.
So much for what it does. The next question is the one a reader has been holding since the first paragraph.
05measured, on a named machine
Faster than Redis, on the platform it is built for
Both quadrants, read and write, in the comparison that is actually like for like: TRIC+
over a Unix datagram socket against Redis over TCP, both serving the single-shot
SET k v EX t and GET k that is Redis's home discipline. No
pipelining, no batching, single-threaded and synchronous on both sides.
FreeBSD 15, AMD Ryzen 5 3600, ZFS, inside a jail. That machine is written down because it is the whole context of the number: FreeBSD is the target this is built and tuned for, and the same code on another operating system is a different measurement that has not been taken here. A benchmark without its machine is a wish.
The in-process figures are a different question and worth keeping apart from the server ones. Without any transport at all, the transient tier reads at about 2.2 million operations a second at 370 nanoseconds, and a cache-promoted read at 2.7 million at 320. Those are the numbers a library user sees; a server user pays for the round trip and gets the ones above.
All of that is close. Is anything not?
06where it is not close
Two operations where the difference is a factor
A three per cent lead on writes is worth reporting and not worth choosing a database over. Two operations are not like that, and both are ones the six primitives make ordinary while the alternative makes them a project.
Against the Redis equivalents on the same machine: a Lua script for
compare-and-delete, and KEYS * for the scan. Characterised in the server manual
under TRIC+-specific workloads.
The reason is not cleverness, it is placement. Compare-and-delete happens on the server
as one operation, where the Redis version is a script shipped to the server to make two
operations look like one. Prefix scan reads an ordered map, where KEYS * walks
everything and discards what does not match. Both differences come from having decided,
early, that these were primitives rather than patterns.
Which leaves the practical question of getting at it from whatever you write in.
07one binary, and the bridges
Ten languages today, and C underneath most of them
One binary does both jobs: tric server starts the daemon and
tric on its own is the command line. Nothing else is installed, and there is no
runtime to provide.
C, C++, Swift, Nim, Lua, Tcl, Zig
Python, JavaScript, TypeScript
Not a bridge at all: the engine is Rust, so it is simply the library
Twenty are planned in four waves, of which two are complete. The rest is enterprise languages and then the remaining ecosystems, each with a client that is idiomatic in its own language rather than a C header in a trench coat. Eight content and shop systems are meant to follow on top of those. This page counts the ten, because ten is what exists.
Tablethe benchmark in full
Three layers, one payload, one method
| Layer and workload | Ops/s | p50 | p99 |
|---|---|---|---|
| In-process, transient write 128 B | 1,645,225 | 500 ns | 1.47 µs |
| In-process, transient read 128 B | 2,199,671 | 370 ns | 770 ns |
| In-process, cache-promoted read | 2,737,603 | 320 ns | 570 ns |
| In-process, SQLite write on ZFS | 18,114 | 23.9 µs | 68.1 µs |
| Server over UDS, write 128 B | 67,570 | 14.5 µs | 22.9 µs |
| Server over UDS, read 128 B | 91,675 | 10.6 µs | 17.5 µs |
| Redis over TCP, write 128 B | 65,383 | 15.2 µs | 16.8 µs |
| Redis over TCP, read 128 B | 79,129 | 12.3 µs | 22.1 µs |
FreeBSD 15, AMD Ryzen 5 3600, ZFS, in a jail. Layer one is the engine with no transport at all, layer two is the server over a Unix datagram socket, layer three is Redis over TCP on localhost. Everything single-threaded and synchronous, no pipelining and no batching on either side. Redis holds the p99 on writes, which is in the table because it is in the results.
Licencewhat you may do with it
Free on one host, and Apache in four years
The server is under a Business Source Licence, which is not an open-source licence and does not pretend to be. What it grants is worth stating plainly, because licences of this family are usually explained in a way that leaves a reader unsure whether they are allowed to start.
- Any single-host production use is free. Not a trial, not a developer edition. If it runs your site on one machine, that is a use the licence grants.
- Every tagged version becomes Apache-2.0 four years after its release. The clock is per version and it is written into the licence, so it does not depend on anybody remaining well disposed.
- What it holds back is the multi-host and hosted-service case, which is the case where somebody else's business is built on this one.
The four-year conversion is the part that makes the rest of it liveable. A licence that merely restricts asks you to trust the licensor indefinitely; one that expires into Apache on a fixed date asks you to wait. The version you deploy today has a date on which it becomes yours outright, and that date does not move.
Edgeswhere it stops
Where it stops, and what that leaves you
- The measurements are FreeBSD measurements. That is the target it is built and tuned for, and the figures on this page were taken there. Linux and macOS are supported and have not been benchmarked here, so the comparison with Redis is a FreeBSD comparison until somebody takes the other one.
- Ten bridges of twenty. Two waves are complete. If your language is in a later wave, the socket protocol is documented and reachable, but the idiomatic client is still on the way.
- Version 0.5, and the source is not published. Every figure here rests on trust until it is, and the benchmark harness ships with the project so the numbers become reproducible the day it ships.
- Six primitives is a decision, not a stage. There are no lists, sets, sorted sets or streams, and there is no plan for them. What composes from six verbs composes; what does not, does not, and a store that wanted to be a data-structure server would be a different product.
- It is not a distributed store. One host, one engine. Replication and consensus are not hiding behind a flag, and a deployment that needs them needs something else.