TessariDB
Get started

The engines

Eleven engines. One substrate.

“One store” is a claim worth being precise about. This is not one engine that does several things loosely, and it is not eleven services behind a façade. It is eleven access paths over one substrate, one transaction and one language. Seven of them change how a record is reached. The other four change something else, which is why they are also the four that are not shorthand for a table and some clauses: a series changes what the store will answer with, a queue changes who may reach a record and until when, a topic makes a message unchangeable once written and numbers it at commit, and a vault changes what is stored — its declared fields are sealed before the record is written, so the index, the change feed, the replication log and every backup carry ciphertext.

Eleven access paths, one substrate. A record, its edge, its terms, its vector and its shape either all land or none of them do.

The whole set

None of these is a plug-in, a sidecar or a second process. Every one of them writes through the same transaction.

EngineHoldsAsked withServed by
DocumentsObjects, arrays, nested treesCREATE · SELECT · UPDATE · DELETEIdentity, a value index, or a scan
GraphIts own records, and the edges between themRELATE, -> <- traversal, bounded by DEPTHThe edge table's own index, or adjacency beside the node
Full-textTerms, produced by an analyzerMATCHES (PREFIX, FUZZY, INFIX), search::score; DEFINE SEARCH across tables; ORDER BY FUSE to rank with meaning or placeA SEARCH index
VectorsArrays of numbers — a store of them, or a field of a record, of a declared widthvector::euclidean and friends, in ORDER BY; APPROXIMATE to ask the indexA VECTOR proximity graph
GeometryShapes on the sphere — a store of places, or a field of a recordEight geo:: predicates, a distance to any shape, an area, geo::cellA SPATIAL cell index
Key-valueOne value per key — optionally expiring, in a space that may hold at most nGET · SET · DEL · KEYS · EXPIRE · TTL · INCR · SET … IFThe key itself
FilesBytes, in chunks, under a ceiling the bucket may declarePUT · READThe bucket's metadata records
Time-seriesEvents ordered by when they happened, answered only within a declared retentionDEFINE SERIES … RETAIN … TIME at · FILL · LATEST BY · ASOF JOIN · DEFINE ROLLUPThe event time in the key, so a late event lands in place and the floor is a position
QueuesOrdinary records, handed out one holder at a time under a hold that lapsesDEFINE QUEUE … TIMEOUT · CLAIM · RELEASEThe deadline the claim writes, which is a field of the record
TopicsMessages that are never changed, at dense positions decided at commitDEFINE TOPIC · CREATE · READ FROM … FOR CONSUMER · DEFINE GROUP · ACK · NACKThe position, each reader's position, and each group's messages in flight, kept in the store
VaultDeclared fields marked SECRET, sealed one key per recordDEFINE VAULT, REVEAL, ADD RECIPIENTThe record's own data key, not an index

Which one you want

The table above says what each engine is. This one starts from the question you arrived with.

What you are trying to doReach forWhy that one
Store an object and read it back by idDocuments, no indexIdentity is already the fastest path there is — a seek, not a search
Find records by the value of a fieldDocuments, a value indexAn index changes what the read costs, never what it answers
Find records by a word inside textFull-textA value index matches the whole field; MATCHES matches terms
Rank text results by how well they matchFull-text, search::scoreScoring needs a collection to measure a word's rarity against
Rank several fields of several tables as oneFull-text, DEFINE SEARCHOne analyzer and one set of statistics, each table's grants kept
Find what is near something in spaceGeometryMetres on the sphere, and predicates that are exact at the boundary
Find what is similar to somethingVectorsSimilarity is distance between arrays of numbers, not a filter
Follow relationships between recordsGraphAn edge is a record with its own index, so it carries data and queries like a row
Walk a graph, where the cost that matters is the hopGraphAdjacency sits beside the node, so a hop does not grow with the number of neighbours
Keep one value under one key, fastKey-valueNo document tree to walk, and the key is the address
Keep bytes that are not a document at allFilesChunked storage, with the metadata as ordinary records
Group events by hour, day or weekTime-seriesThe window is anchored to the epoch, so every process asking for 1h agrees where it starts
Keep hourly totals as the raw events arriveTime-series, DEFINE ROLLUPKept in the transaction that writes the event, so the totals and the events never disagree
Keep only the last thirty days, and have reads say soTime-series, DEFINE SERIES … RETAIN 30dThe floor governs the answer; removing the bytes is a separate act, so a lagging pass costs storage and never correctness
Hand a piece of work to exactly one worker at a timeQueuesThe hold lapses on its own, so a worker that dies returns its work without anything having to notice
A session that times out, a rate-limit window, a lock that frees itselfKey-value, EXPIRE · INCR · SET … IF ABSENTThe expiry is in the key's version, so every node and every restart agrees on it
Publish events several parts of the application read, each at its own paceTopicsA reader's position commits with what it writes here, so an effect in this store happens once per message
Rank by the words typed and by what they meantFull-text with ORDER BY FUSEFused by place in each order, never by adding a score to a distance
Keep a credential the database itself cannot read backVaultThe field is sealed before the record is encoded, so every copy downstream carries ciphertext

Declare the engine, or reach it from a table

When an engine is what you are storing, declare the store as one. That is the route in: one word, and the parts that have to agree with each other already do.

One word, and you can write into ittessariql
DEFINE GRAPH social;
CREATE social:1 = { name: 'ada', team: 'core' };
DEFINE EDGE knows IN social FROM social TO social;

DEFINE VECTOR embeddings DIMENSION 768 DISTANCE cosine;
CREATE embeddings:'intro' = { vector: [0.1, 0.2, 0.3], label: 'intro' };

DEFINE GEO places;
CREATE places:'paris' = { geometry: geometry { type: 'Point', coordinates: [2.35, 48.85] } };

No table is declared first, because the word declares one. A declared engine you cannot write a record into would be a label rather than a store.

The other route stays, and for most records it is still the right one: a vector, a shape or an edge as a field of a table that exists for its own reasons. A chunk with an embedding is still a chunk. Tables you already have join a graph with IN social rather than being replaced by it.

Two things those tables are not saying

These are not alternatives

One record can carry a text field, a vector, a shape and an edge at once, and one transaction writes all of them. The question is never which engine is this database but which access path answers this read — and a single statement may use several.

Only one trades exactness for speed

An index changes what a read costs, never what it answers. That holds for every index here with one exception — the vector index, which cannot promise it, and so the statement has to ask for approximation out loud.

One statement, planned once. Three access paths carry it — a full-text index, a field on the records themselves and a vector index — and the five engines it does not need are never consulted. There is no second process in this picture, and no network hop between any two boxes in it.
Exact until the statement says otherwisetessariql
SELECT * FROM points ORDER BY vector::euclidean(at, [0.1, 0.0]) LIMIT 2;
-- the exact scan it always was

SELECT * FROM points ORDER BY vector::euclidean(at, [0.1, 0.0]) LIMIT 2 APPROXIMATE;
-- now the proximity graph, and now it may be wrong

Which matters more than it looks: the ground truth you measure your own recall against comes from the same store as the answer, rather than from a second system you would have to trust.