TinyStore

Reference / The wire protocol, version 1

The wire protocol, version 1

The bytes a TinyStore server and its clients exchange over any byte stream: a child's stdin and stdout, a Unix socket, a Windows named pipe, TCP or TLS. Read it to write a client in a language TinyStore has no SDK for, or to see what an SDK does on the wire; the Bun and Python SDKs are built to this page.

Frames, the profile, the handshake, the errors and every engine's methods, kv, jobs, blobs, sql, records and metrics, are built in server/wire, and why the server behaves as it does is its design. Every example on this page is a vector of testdata/wire/vectors.json, which the server and every SDK are tested against: a value in a typed notation, the bytes it is, and the bytes a decoder refuses, each named after the rule it breaks. Every message is also a vector of messages.json, its bytes and its fields by the names the tables below give them, with the methods' numbers and the errors' codes, so that an SDK checks each field of each message in one loop; the Go types write it, through a schema of each message's keys that TestMessageVectors holds.

Frames

Everything is little-endian. A frame is a twelve-byte header and a body:

0             4      5       6          8             12
│ body length │ kind │ flags │ method   │ stream      │ body …
  u32           u8     u8      u16        u32
  • The length is the body's. A frame is 12 + body length bytes; a body past the agreed maximum is refused before a byte of it is read.
  • What is reserved is zero. An unknown kind, a flag this kind does not define, a method outside a REQUEST or a stream where the kind names none is a protocol error, and the connection ends.
kindstreammethodbodyflags
1HELLO00a message—
2WELCOME00a message—
3REQUESTthe new streamthe operationa messageEND
4RESPONSEits stream0a message, or an errorEND, ERROR
5DATAits stream0bytes, or a message, as the method saysEND, ERROR
6CANCELits stream0empty—
7CREDITits stream, or 0 for the connection0u32: bytes granted—
8PING008 bytes—
9PONG00the PING's 8 bytes—
10GOAWAY00a message—

END is 1: the sender's last frame on its stream. ERROR is 2, only beside END: the body is an error.

A method's high byte is its engine and its low byte the operation, so a new engine takes a new high byte and the frame stays as it is:

high byteengine
0x01kv
0x02jobs
0x03blobs
0x04sql
0x05records
0x06metrics

A HELLO from Bun:

16 00 00 00  01  00  00 00  00 00 00 00                                   a body of 22 bytes, HELLO
82                                                                        a map of two
   01 01                                                                  protocol: 1
   02 b1 74 69 6e 79 73 74 6f 72 65 2d 62 75 6e 2f 30 2e 31               client: "tinystore-bun/0.1"

A connection

  1. The client sends HELLO, and the server answers WELCOME, or GOAWAY and closes. Nothing comes before them, and the server waits five seconds for HELLO. The connection speaks the older of the client's newest protocol and the server's, which WELCOME names: a client newer than its server speaks the server's protocol, or closes when it no longer does, and only a client older than the oldest protocol the server speaks is refused.
  2. The client opens streams with REQUEST, as many at once as WELCOME allows.
  3. Either side may send PING; the other answers PONG with the same eight bytes.
  4. The server ends the connection with GOAWAY. The client opens no stream after it; a REQUEST that crossed it on the way is answered unavailable without being run, so it may be sent again elsewhere; the streams already running finish, and the server closes. Stream numbers are reused, so no number could say which requests the server saw: each answer says it.

HELLO:

keyfieldtype
1protocoluintthe newest the client speaks, 1
2clientstra name and version, for logs
3tokenstrrequired on TCP
4max bodyuintthe largest body the client takes; the server's when absent
5stream credituintthe DATA the server may send on a stream before the client grants more; 2 MiB when absent
6challengebin16 random bytes, when the client found the server through SERVE

A body must fit the credit it is sent under, so the max body both sides agree is the smallest of the server's, the client's and the client's stream credit: a client granting 64 KiB a stream takes bodies of 64 KiB at most.

WELCOME:

keyfieldtype
1protocoluintthe one this connection speaks
2serverstrits version
3instancebin16 random bytes a start
4capabilitystradmin, data or read
5max bodyuintthe largest body either side sends
6in flightuintthe streams a client may have open at once
7connection credituintthe bytes of REQUEST and DATA bodies a client may send before credit comes back; at least the max body
8stream credituintthe bytes of DATA a client may send on a stream before credit comes back
9enginesarray of strwhat this server serves
10nowintthe store's clock, unix milliseconds
11proofbinon a local connection whose HELLO carried a challenge: the HMAC-SHA256 of the challenge, keyed with the 32 bytes of SERVE's secret
12durabilitystrfull or os, when the store was opened with one: how far its files' commits go before they return

A client that found the server through SERVE sends no REQUEST before the proof checks, compared in constant time: only the store directory's owner can read SERVE, so a process that took the endpoint of a server gone cannot answer, and a client whose proof fails reads SERVE again, as a stale one. HELLO carries nothing a stranger could use, since a local connection takes no token. A challenge of another length is a HELLO the server cannot take; a remote connection's challenge gets no proof, its server being the one its TLS certificate names. The vectors hold a proof, proofs, and a HELLO with a challenge.

A server that does not take the connection at all, past its connections or closing, still reads the HELLO and answers GOAWAY where the WELCOME belongs, with limit or unavailable.

GOAWAY:

keyfieldtype
1codestras an error's
2messagestr

Finding a local server

A store directory has at most one server, since its LOCK lets one process open it. That server publishes how to reach it in <dir>/server/, which only the directory's owner may enter: mode 0700 and, on Windows, which lets anyone traverse a directory to a file whose path they know, a protected DACL naming its owner alone, which every file made in it inherits. SERVE there is a hint; LOCK is the truth.

json
{"protocol": 1, "server": "0.4.0", "pid": 4212, "instance": "q1q0l3yZ3JvYw8g7oM2sYA",
 "secret": "Zm9vYmFyYmF6cXV4Zm9vYmFyYmF6cXV4Zm9vYmFyYmE",
 "endpoints": ["unix:///srv/app/data/server/tinystore.sock"], "sidecar": true}
field
protocolthe newest protocol the server speaks
serverits version
pidits process, for a person reading the file; no client trusts it, since a pid is reused
instance16 random bytes a start, base64url without padding, which WELCOME repeats
secret32 random bytes a start, base64url without padding, the key of the proof
endpointswhere it listens: unix:// and a socket's path, or pipe: and a Windows named pipe's name
sidecartrue when the server is a sidecar, which tinystore serve --local starts for its clients. A client of a newer release may replace a sidecar. The field is absent for a Go program's own server and for tinystore serve <dir>, which a person runs
  • Written whole, under the lock. Only the store holding LOCK writes server/. A server removes a SERVE left behind before it listens, since the endpoint that file names may be another process's by now, writes its own once it listens, as a temporary file renamed into place, and removes it when it leaves, before it lets go of LOCK.
  • Find or start. A client reads SERVE, connects to an endpoint and checks the proof. Any failure, no file, a refused connection or a proof that does not check, starts `tinystore serve --dir --local --log /server/serve.log`. That child takes `LOCK`, which means the old server is gone, and publishes itself; or it finds `LOCK` held and exits with code 3, and the client reads `SERVE` again until the winner answers, five seconds at most, starting one again when none does.
  • The proof. A client that found its server through SERVE sends a HELLO with 16 random bytes as its challenge, and sends no REQUEST until WELCOME's proof equals the HMAC-SHA256 of the challenge keyed with the secret, compared in constant time. An endpoint's name is anyone's to take once its server is gone, a Windows pipe's above all, and only the directory's owner can read the secret. A client whose proof fails reads SERVE again, as a stale one.
  • A reader delays a change and never fails it. On Windows a client holding SERVE open refuses its rename or removal, and so may a scanner; the server tries again eight times, from 1 to 64 ms apart.

How a client knows its server depends on the transport:

transporthow the client knows its server
stdioit started the server itself, as a private child
a Unix socket or a named pipe found through SERVEthe proof
TCP with TLSthe certificate, checked before its token leaves
TCPnot at all: the token travels in the clear, on a network its operator trusts

Where a local server listens:

  • A Unix socket, <dir>/server/tinystore.sock, when that absolute path fits a sockaddr_un, 104 bytes on macOS and the BSDs and 108 on Linux; past it, the same name in a directory of the owner's own, $XDG_RUNTIME_DIR/tinystore-<hash>, or in the temporary directory without one. <hash> is the first eight bytes of the SHA-256 of the store's absolute path, in hex: eight, so that a socket under macOS's temporary directory still fits.
  • A named pipe on Windows, pipe:tinystore-<hash>, created as the first instance of its name with a DACL naming its owner alone, which refuses remote clients. No local endpoint takes a token: its permission is the file system's, and a local connection is admin.

A remote server's tokens file gives each token one capability: admin may do anything, data reads and writes, and read reads only. A read connection may call only the methods the schema marks read, every other one being permission; its SQL runs on a reader and is a query, a statement that writes, attaches a file or sets a pragma being permission too; and its sql.open applies no migration. Its opens may still name a bucket, a queue or files the store did not have, as any first open does, and its reads of an idle key renew it, as any read does.

Streams

A client names a stream with any number from 1 not in use. A number is in use from its REQUEST to the server's final frame on it, and may be named again after. A REQUEST on a number in use, or past the streams in flight, ends the connection.

shapethe client sendsthe server sends
a callREQUEST·ENDRESPONSE·END
an uploadREQUEST, DATA…, DATA·ENDRESPONSE·END
a downloadREQUEST·ENDRESPONSE, DATA…, DATA·END
both waysREQUEST, DATA…, DATA·ENDRESPONSE, DATA…, DATA·END
  • The server ends every stream once: RESPONSE·END or DATA·END, either with ERROR, a cancelled stream too. It takes the number out of use before that frame leaves, so a client may name it again the moment it arrives.
  • A download's RESPONSE is its header, a message of the method's, {} when it has nothing to say; its items follow as DATA, and its last DATA·END is its trailer, a message saying where the next page begins.
  • DATA carries bytes or items. A blob's bytes are raw; rows, jobs and series are a message each. The method says which. A DATA carries at least one byte, but the one that ends its stream.
  • A transfer shares the connection. A sender splits bytes into frames of 64 KiB at most, so that no stream holds the connection longer than one such frame takes to send.
  • CANCEL asks the server to stop. It answers with the final frame it would have sent, or with the error cancelled; a client may drop what arrives on a stream it cancelled. A client ends an upload early with CANCEL: ERROR is the server's alone.
  • A frame for a stream no longer in use is dropped: a DATA the client sent before the final frame reached it, a CANCEL or a CREDIT that crossed it. The dropped DATA still gives its connection credit back.
  • A body that is not its method's message fails its stream with invalid, naming the rule it broke; a frame that is not a frame ends the connection.

Credit

A sender never sends more than it was granted, and a receiver grants only room it has, so a connection holds no more than its grants and the server's reader never waits.

  • Client to server. Every REQUEST and DATA body counts against the connection's credit, and a DATA body against its stream's too. The server gives credit back with CREDIT as it lets go of bodies: on stream 0 for the connection, on a stream's number for that stream.
  • Server to client. DATA bodies count against the stream credit HELLO stated, and the client grants more with CREDIT on the stream as it consumes them. A RESPONSE body is not counted: it is one message within the agreed body, and every collection travels as DATA instead, so a client holds at most its streams in flight times the agreed body in answers. Nor is the DATA·END·ERROR that fails a download, so that a stream always has room for its end. A client grants nothing for the connection: its CREDIT names a stream.
  • A grant waits for half the window. A receiver grants what it let go of once half its window has gone, and not a frame for every frame it takes.
  • A body past its credit ends the connection with GOAWAY and protocol, the connection's last frame.

64 KiB of credit on stream 5:

04 00 00 00  07  00  00 00  05 00 00 00                                   a body of 4 bytes, CREDIT, stream 5
00 00 01 00                                                               65,536

Messages: the MessagePack profile

A message is one MessagePack value: a map from small unsigned integer keys to values, under these rules.

  • Types. Nil, bool, integers, float 64, str, bin, array and map. A float 32, an extension type, the timestamp among them, or any other type byte is invalid.

  • Integers. Each field says signed or unsigned and its range. A decoder takes any encoding of a value that fits, ce 00 00 01 2c for 300 as well as cd 01 2c, and refuses one that does not.

  • A float keeps its value; bits that must survive are bin. A float field is a float 64: cb 80 00 00 00 00 00 00 00 is -0. The server keeps its bits, so cb 7f f8 00 00 00 00 00 01, a NaN whose payload is 1, comes back as it went; an SDK keeps the value, NaN, the infinities and -0 included, as far as its language does, and JavaScript promises no NaN's payload. A float whose bits are the data therefore travels as bin: a kv float value, a series' samples, an aggregate's values. A float field also takes an integer that a float 64 holds exactly, since a JavaScript encoder writes 3.0 as 3.

  • Text is str, bytes are bin. A str is valid UTF-8, and neither stands for the other.

  • Keys. A message's keys are unsigned integers; a field that is a map of names, an error's what or a series' labels, says so and has str keys. A key twice is invalid. A key the reader does not know is read past, its value still well formed and within bounds, and then it depends on the reader. A server refuses a request carrying one, unimplemented naming the field, since a field it skipped could change what the client asked, and the answer would be to another question. A client reading an answer, and a server reading HELLO, skip it. So a client leaves out a field at its zero value, and an older server takes everything a newer client sends that it would have sent anyway; and a server adds an answer field only where a client that skips it still reads the answer right, or else answers it only to a client that asked for it in a field of its request. An absent key is the field's default, and nil stands only where a field allows it.

  • Bounds come before allocation. A length or a count past the bytes left in the body is invalid, since every element takes at least a byte; nesting deeper than 8 is invalid; a body is at most the agreed maximum.

  • Time is an integer: unix milliseconds, unless a field says nanoseconds. Records keep nanoseconds, past what a JavaScript number holds, so an SDK reads such a field as a BigInt:

    1,790,000,000,123,456,789 ns → cf 18 d7 5b 84 2b 4e cd 15

    A duration is an integer of milliseconds.

  • Columns of numbers are bin: a series' times and values, little-endian int64 and float64 one after another.

  • Encoders are canonical, decoders are not. An encoder writes the shortest form of every integer, length and count, keys in ascending order, every float in eight bytes, so that a vector compares byte for byte; a decoder takes any valid form in any order.

  • An SDK's codec is its own: a general library writes plain maps and reads 64-bit integers without rounding when told, but takes a float 32, an extension, a key twice or a value nested nine deep, which this profile refuses. The SDKs' codecs refuse every refused vector.

Errors

An error is the final frame of its stream, with ERROR set:

keyfieldtype
1codestrbelow
2messagestrwhat failed, as the engine's error says it
3whata map of namesthe item it names: a bucket and key, a queue and key, a series' labels, a table and constraint
codemeanssent again
invalidthe request cannot be done as askedno
limita bound: memory, size or count; what names the limit, what the call wanted of it and the bound, where the engine knows themlater, or smaller
closedthe store or the handle closedafter opening again
in_usea name is taken, or a lock's key stayed held past a hold's waitlater
conflicta condition or a version no longer holdsafter reading again
corruptstored bytes no longer readno
too_old, too_newa time outside its engine's windowno
suspendeda series in quarantineafter its repair
outcome_unknowna commit whose result is unknownafter reading what it wrote
committed_locallya synchronous commit its home made and no copy it waits for held in time: it stands, and losing that home's disk would lose itno
losta lease ran out or was revoked at its maxHold: another holder may have run beside itno
permissionthe connection's capability does not allow it, as a read connection opening an encrypted bucketno
unimplementeda method, or a field of a request, this server does not have: what names the field, the message the server's versionno
cancelledthe client cancelled it—
unavailablethe server is closingon another connection
internala fault of the server's—

The first nine are the root's sentinels, so that an SDK's error classes mean what errors.Is means in Go. A frame or a HELLO the server cannot take is a GOAWAY with protocol or unauthenticated, and the connection ends. A name's value in what is str, or bin where its bytes are not UTF-8, as a kv key of bytes may be.

A conflict on stream 7:

2c 00 00 00  04  03  00 00  07 00 00 00                                   a body of 44 bytes, RESPONSE, END and ERROR, stream 7
83                                                                        a map of three
   01 a8 63 6f 6e 66 6c 69 63 74                                          code: "conflict"
   02 ad 73 74 61 74 65 20 63 68 61 6e 67 65 64                           message: "state changed"
   03 81 a6 62 75 63 6b 65 74 a8 73 65 73 73 69 6f 6e 73                  what: {"bucket": "sessions"}

Versions

A server and its clients upgrade apart: a remote server deployed once while the applications' SDKs move on, or a sidecar still serving a directory after its application's SDK was updated. So each side promises what it does with an older or a newer one:

  • A connection speaks the older protocol of its two sides. HELLO says the newest the client speaks and WELCOME the one the connection speaks; a server refuses only a client older than the oldest protocol it still speaks. A protocol moves only for what a field cannot carry: frames, the handshake, credit.
  • Within a protocol a message grows by fields, and a request is understood whole or refused. A field the server does not know is unimplemented, naming the field and the server's version, so a newer client using something new learns which server to upgrade rather than reading an answer to another question. A client leaves a field out at its zero value, so one using nothing new talks to any server of its protocol.
  • An answer grows only by what a client may skip. A field whose meaning a client must know is answered only to a client that asked for it with a field of its request, which an older server refuses.
  • A method is added, never repurposed. An older server answers a newer method unimplemented, naming its version. From v0.1.0 on, a message never changes meaning and a field's number is never reused.

A client that finds a sidecar of an older release than its own replaces it:

  1. The client sends server.stop. The sidecar answers, lets its running streams finish, and gives back SERVE and LOCK.
  2. The client starts its own binary, as it does when no sidecar runs, and prints one line about it on stderr.
  3. Every other client of the old sidecar gets GOAWAY and connects to the new sidecar when it connects again.

A client doesn't replace a server that SERVE doesn't call a sidecar, such as a Go program's own server or one that a person runs. It also keeps a sidecar that refuses to stop. In both cases, it prints a warning once on stderr.

Methods

Each engine's messages are fixed on this page with its slice, before its code: numbers, keys, types, shapes and vectors. A call answers with one message; whatever returns a collection is a download, its items DATA under credit and its last DATA saying where the next page begins. What each will carry:

engineoperationsshape
the serverstopa call
kvopen, get, has, set, delete, take, touch, add, max, clear, batch, view, allow, configurecalls
scana download, an entry a message
watcha download that does not end: a config's kept fields, again after each change
runboth ways: the answer a key keeps, or the key's run handed over and its answer back
usage, refundcalls on a quota, which allow uses
jobsopen, enqueue of many, update, cancel, get, claim, settle of many, step, keepcalls
scana download, an entry a message
watcha download that ends with its job: its entry, again after each change
workboth ways: jobs out, their outcomes back
blobsopen, stat, delete, copy, move, usage, clearcalls
scana download, an object a message
putan upload
get, with an offset and a lengtha download; a whole read whose bytes do not match its hash ends with ERROR and corrupt instead of END
sqlopen, with its migration files; exec; batchcalls
querya download: the columns, then rows a message at a time
recordsappend of many, dropcalls
read, followa download, records a message at a time
linesan upload of another program's output
metricsingest, dropcalls
read, aggregatea download, a series a message, or several for a series longer than a body holds
searchopen, set, setMany, delete, get, search, status, dropIndex, listcalls

The values they carry:

  • the server's own call carries nothing, {} each way;
  • a kv value is nil, an integer or bin, as its row keeps it, a bin's first byte the kind that wrote it (1 text, 2 bytes, 3 JSON, 4 a float's eight bytes), and a version is an opaque bin compared only for equality;
  • a job's value is its JSON as str;
  • an SQL value is one of SQLite's five: nil, an integer, a float, str or bin;
  • a record's attribute keeps its JSON spelling as str;
  • a series' samples, and an aggregate's buckets and values, are bin columns.
  • a search's document is its JSON as str, and a vector bin, four bytes a little-endian float a dimension.

the server

The server's own methods take the range below the engines', 0x00xx.

methodrequestanswer
0x0001stop{}{}, then the server stops as its closing does: the streams running finish, every connection is told GOAWAY, and a sidecar gives back SERVE and its directory
0x0002clocka clock: a time to set it to, a while to move it forward by, or neither to read ita clock: the time it reads once moved
0x0003backup{1: files}a download: {}, then the zip of the whole store in DATA, its bytes as they come, the last with END

A stop is an admin connection's alone, permission to a data connection, and a server whose program said nothing of stopping refuses it with permission too: a program serving its own store decides when that store's server stops. tinystore serve stops at one, as it does at Ctrl+C, which is how tinystore stop ends a sidecar and how an SDK newer than the sidecar it found replaces it with its own binary, as Versions says.

Only an admin connection can ask for a backup. The server first opens each engine whose file is in the directory. It copies each database that no client has opened, without opening it. Then it sends what the backup package writes: a copy of every engine's file, and a manifest of their sizes and checksums, which a restore checks. files, an array of str, names files of the application's by their paths inside the store's directory, which the zip holds beside the engines' with the engine host; a path outside the directory, or an engine's own, is invalid.

A clock is {1: at, 2: advance}. at is a time in unix milliseconds, and advance is a duration in milliseconds. Only a private server started with tinystore serve --stdio --clock <time> has a clock that moves, and the store runs on it instead of the system's clock. An SDK starts such a server when you open a store with private and clock. Only an admin connection can move the clock, and only forward. The server answers invalid to a time before the clock's, a negative duration, both fields at once, or a clock call to a server that runs on the system's clock.

kv

kv.open answers a handle on a bucket of values, on counters, on a config, on a limiter, on once's answers or on a quota, and every other call carries it. A handle holds kv.Raw values, so the server reads what any bucket wrote.

methodrequestanswer
0x0101opena bucketa handle
0x0102geta callan entry: found, value, version, expires; on counters the value, 0 when absent
0x0103hasa callan entry: found
0x0104seta call with its valuean entry: found, version, expires; with if absent, found is false when a live key was there, and the entry is that key's
0x0105deletea call{}
0x0106takea callan entry: found, value
0x0107toucha call with ttl or expire atan entry: found
0x0108adda call with nan entry: the counter's value
0x0109maxa call with nan entry: the counter's value
0x010acleara call naming a branch{}
0x010bbatchcalls, in one transactionresults; a call that fails fails them all, and what names it as call
0x010cviewcalls, get and has, from one snapshotresults
0x010dscana call naming a branch, after and limita download: {}, an entry a DATA with its key, a page
0x010eallowa call on a limiter or a quota: a key, and n requests or uses, 1 when absentan allowance
0x010fconfigurea config's fields to keep and paths to forget{}
0x0110watcha call on a configa download that does not end: {}, then the kept fields a DATA, now and after each change
0x0111runa call on once's answers, whose REQUEST leaves the client's side openthe answer kept, found, which ends the stream; or not found, the run handed over: the client's last DATA is the entry to keep, and the server's, {}, follows once it is kept
0x0112usagea call on a quota: a keyan allowance, nothing used: ok says one more use would pass
0x0113refunda call on a quota: a key, and n uses, 1 when absent{}

A bucket:

keyfieldtype
1namestr[a-z0-9][a-z0-9_-]{0,63}
2countersboolcounters rather than values
3default ttluintmilliseconds
4slidinguintmilliseconds; values alone
5lose at mostuintmilliseconds; counters alone
6configboola config rather than values
7rateuinta limiter's requests every per; a bucket with a rate is a limiter
8peruintmilliseconds
9burstuintthe requests a limiter lets through at once; rate when absent
10onceboolthe answers kv.run keeps, a day unless default ttl says; get and delete read and forget one
11windowsarray of windowsa quota's, one to eight: a bucket with windows is a quota, which delete also takes
12instra SQL database's name, already opened; omitted for kv.db

A window is {1: name, 2: limit, 3: per}: a key may use up to limit every per milliseconds from its first use, the name [a-z][a-z0-9_]{0,31}.

A handle is {1: uint}. A call:

keyfieldtype
1handleuint
2ownersarray of keysthe branch below the bucket's root; absent for the root
3keya key
4valuenil, int or binnil when absent
5ttluintmilliseconds
6expire atintunix milliseconds
7if versionbina version that an entry carried. A get or a has with it fails conflict if the key no longer has that version
8if absentboolA get or a has with it fails conflict if the key holds a value
9nintwhat add adds, and max compares
10aftera keythe key a scan's page begins after
11limituintthe keys a page returns: 100 when absent, 1000 at most

A key is text: a str, a bin, or an integer, which is its decimal spelling, so that 42 and "42" name one key. The server writes a key as str, or as bin when it is not UTF-8. An entry:

keyfieldtype
1foundbool
2valuenil, int or bin
3versionbin
4expiresintunix milliseconds; absent for a key that never expires
5keya keya scan's item alone

A limiter answers kv.allow with an allowance, {1: ok, 2: left, 3: retry after}: whether the requests pass, how many more would pass now, and how many milliseconds until they would, rounded up, when they do not. More requests than the burst at once are invalid. A quota's allowance adds 4: windows, each {1: name, 2: used, 3: limit, 4: left, 5: reset at} in the order its open gave them, reset at in unix milliseconds and absent for a window not started; its kv.allow counts in every window or in none, and more uses than a window's limit are invalid.

kv.configure is {1: handle, 2: set, 3: reset}, set an array of str, a path and its JSON each, and reset the paths to forget; the server keeps both in one transaction, checking only that a value is JSON within the config's bounds, since the types are the client's. kv.watch answers {}, then {1: changes, 2: fields} a DATA, fields a path and its JSON each, now and after each change, whoever made it: a watcher behind is sent the latest fields, never a state the config did not have. It ends when the client cancels it, when the client's side of the connection ends, or with the server.

kv.run lets one run of a key go at a time, every client's: another kv.run of the key waits until that run ends, then is answered what it kept, or is handed the run when it kept nothing. The client that is handed a run, {} as its RESPONSE, runs its function and sends an entry as its last DATA: found with the value to keep, or not found when the function failed, which keeps nothing. A client that cancels or leaves keeps nothing either, and the next run of the key is handed over again.

A page, a scan's trailer, is {1: more, 2: after}: more says the limit or the page's 4 MiB of values ended it before the branch did, and after is the key the next page begins after. Calls are {1: [call…]}, each call's map holding its method under key 0, and results {1: [entry…]}.

kv.get of token under the owner 7, on handle 1, and its answer:

0e 00 00 00  03  01  02 01  01 00 00 00                                   a body of 14 bytes, REQUEST, END, 0x0102, stream 1
83                                                                        a map of three
   01 01                                                                  handle: 1
   02 91 a1 37                                                            owners: ["7"]
   03 a5 74 6f 6b 65 6e                                                   key: "token"

19 00 00 00  04  01  00 00  01 00 00 00                                   a body of 25 bytes, RESPONSE, END, stream 1
84                                                                        a map of four
   01 c3                                                                  found: true
   02 c4 05 68 65 6c 6c 6f                                                value: bin "hello"
   03 c4 01 31                                                            version: bin "1"
   04 cf 00 00 01 a0 c4 50 6c 00                                          expires: 1,790,000,000,000

jobs

jobs.open answers a handle on a queue of JSON values, or on a schedule, the queue of one repeating job under its name; every other call carries it. A job's value is its JSON as str, checked before it is kept; a schedule's is {}.

methodrequestanswer
0x0201opena queuea handle
0x0202enqueuea batch: jobs one transaction adds, all or none{}; what names a refused job as call
0x0203updatea change: a job by its key, a new value, time or repeat{}
0x0204cancela keyan entry: found says there was a job, which a running one's handler is told
0x0205geta keyan entry
0x0206claima leasea held job, found false when none was due
0x0207settleoutcomes of claimed jobs, written in one groupsettled: nil or an error each
0x0208scana querya download: {}, an entry a DATA, a page
0x0209workworkersboth ways: {}, then held jobs out and outcomes back, DATA·END each side
0x020awatcha keya download: {}, then an entry a DATA, now and after each change, until the job ends
0x020bstepan answer's job and namekept: the answer the job's run kept under the name, found false for none
0x020ckeepan answer{} once the answer is written, the attempt holding the job's lease

A queue:

keyfieldtype
1namestr[a-z0-9][a-z0-9_-]{0,63}
2leaseuintmilliseconds; 30 seconds when absent
3max attemptsuint10 when absent
4backoff firstuintmilliseconds, the first wait after a failure; a second when absent
5backoff mostuintmilliseconds, the longest; an hour when absent
6max waitinguintten million when absent
7keep faileduintmilliseconds; seven days when absent
8keep doneuintmilliseconds; absent forgets a key when its job is done
9schedulea repeata schedule rather than a queue
10max runninguintthe jobs that may run at once, across every worker of the store; absent bounds none
11instra database's name. The queue lives in that database's file instead of jobs.db, and the database's batches can enqueue on it. A client opens the database first, on any connection. Absent means jobs.db
12max running in groupuintthe jobs of one group that may run at once, across every worker of the store; absent bounds none
13rateuintthe jobs that may start in any span of per; absent bounds none
14peruintmilliseconds, the rate's span, at least one

A repeat is {1: cron, 2: zone}, five cron fields and the zone's name, or {3: every, 4: spread}, milliseconds, at least a second; spread true runs each key's job at a phase of its own within the interval, which the job's repeat text then shows, @every 30s +6178ms. A job, a batch's item:

keyfieldtype
1valuestrits JSON
2keystr1 to 1024 bytes
3atintunix milliseconds; a time past runs now
4afteruintmilliseconds from now
5repeata repeatneeds a key
7moveboolsets the time of its key's job either way: a waiting one moves, a running one runs again then, and a key without a job gets one; needs a key
8groupstr1 to 1024 bytes: the group whose running jobs max running in group bounds

A batch is {1: handle, 2: [job…]}; a change is a job's fields with the handle under key 6; a key is {1: handle, 2: key}. An entry:

keyfieldtype
1foundbool
2keystr
3valuestr
4atintunix milliseconds: the time it runs for
5attemptuintits attempts, a running one included
6stateuint1 waiting, 2 running, 3 failed, 4 done while keep done keeps its key, 5 cancelled, which only a watch sends
7errstrits last failure
8repeatstra repeating job's cron text and zone, as jobs.db keeps it
9aheaduintthe jobs that run before a waiting one, up to 10,000; get and watch alone
10progressstrwhat a running job's worker last reported, as JSON
11ranintunix milliseconds: when the last run a worker finished began
12tookuintmilliseconds: how long that run took

A job a work loop claimed ahead for a busy worker is waiting, 0 ahead, until a worker has it. A lease is {1: handle, 2: lease}, milliseconds, the queue's when absent. A held job is {1: found, 2: job, 3: key, 4: value, 5: at, 6: attempt, 7: cancelled}, job being the number its outcome names: a claim's lives on its connection until it is settled, and a work stream's on its stream. An outcome:

keyfieldtype
1jobuint
2howuint1 ack, 2 retry, 3 fail for good, 4 snooze, 5 extend, 6 progress
3errstrwhy a retry or a failure
4atintunix milliseconds: when a retry or a snooze runs again
5afteruintmilliseconds: the same from now, or how long an extend holds; written when given, 0 too, which runs a retry or a snooze now where one without a time waits its backoff
6progressstra progress's report, any JSON, which get and watch show until the job is settled

A progress settles nothing: the job stays in its worker's hands, claimed or on a work stream, and a report that is not JSON is invalid. The server keeps the last one in memory, 4 KiB of JSON at most, a larger one dropped and logged, so the SDKs refuse one past it before it leaves.

Outcomes are {1: [outcome…]} and settled {1: [nil or error…]}; settling a claimed job a cancel took is conflict. A query is {1: handle, 2: prefix, 3: state, 4: after, 5: limit}, state 1, 2 or 3, and a page {1: more, 2: after}, as jobs.Query and jobs.Page are.

Workers are {1: handle, 2: workers, 3: timeout, 4: until idle, 5: cancels}: the queue's own Work loop runs for the stream, workers at once, 1 when absent and at most the streams in flight, each job timeout milliseconds in the client's hands, a minute when absent, and with until idle only until no job is due and none runs, as a test wants it. Each job it hands a handler goes out as a held job; the client settles it by sending its outcome as DATA on the same stream, and ends its side with DATA·END once it takes no more. An extend is not one of those outcomes: the loop extends its leases itself, and ends the stream invalid on one rather than take it as an ack. A progress is, and leaves the job in the client's hands. When that side ends, or the client cancels or leaves, the jobs in its hands fail that attempt, as a worker whose process died fails it, and only after their handlers return does the loop stop, so that the jobs it claimed ahead and never handed over go back uncounted. The server's DATA·END, {}, follows.

A cancel that takes a job in the client's hands sends it again as {2: job, 7: true} on a stream that asked with cancels, so that its handler stops; a stream that did not ask is told nothing. Either way the outcome the client sends for that job settles nothing, and the stream goes on.

A held job's number is its connection's, the same for a claim's and a work stream's, so that jobs.step and jobs.keep name either; a work stream's job is settled on its stream alone, and jobs.settle of one is invalid. An answer names a step of the held job's run:

keyfieldtype
1jobuintthe held job's number
2namestrthe step's, 1 to 256 bytes, within the run
3answerstrjobs.keep's: the step's answer as JSON, at most 1 MiB; jobs.step carries none

and kept is {1: found, 2: answer}. A step kept by an attempt whose lease another claim took is conflict, and nothing is kept; a run that ends takes its steps along, and a repeating job's next run starts without them. A worker asks jobs.step before it runs a step and sends jobs.keep once it has, so that the attempt after a retry or a lost worker gets the answer back without running the step again.

jobs.watch answers {}, then the job under the key as an entry a DATA, now and each time its state, place, attempt, time, progress or error changes, and ends after the entry in which the job ended: done, failed or cancelled. A key that names no job ends it at once. A watcher behind a busy queue is sent the latest entry, read at most five times a second, never every change. It follows the job it found, so a later job under the key is another watch's, and it also ends when the client cancels it, when the client's side of the connection ends, or with the server.

blobs

blobs.open answers a handle on a bucket; every other call carries it, the owners a call names being the folder under the bucket's root it works in.

methodrequestanswer
0x0301opena bucketa handle
0x0302stata callan object, found false for a key that holds none
0x0303deletea call, if match its one option{}
0x0304copya call from key to to, with the destination's optionsthe object written
0x0305movethe samethe object written
0x0306usagea call naming a foldera total
0x0307cleara call naming a folder{}
0x0308scana call naming a folder, prefix, after and limita download: {}, an object a DATA, a page
0x0309puta call with the object's optionsan upload: its bytes as DATA, 64 KiB at most each; the object committed
0x030ageta call, with an offset and a length for a rangea download: the object, then its bytes as DATA; or the object alone, found false, with END

A bucket is {1: name, 2: default ttl, 3: max size}, milliseconds and bytes. A call:

keyfieldtype
1handleuint
2ownersarray of str or intthe folder's segments
3keystra path
4tostrcopy's and move's destination
5content typestrabsent keeps what a copy's source has
6metaa map of namesabsent keeps it; present replaces all of it
7ttluintmilliseconds
8expire atintunix milliseconds
9sizeuinta put's length: a stream that disagrees leaves nothing
10if matchstras an HTTP If-Match header spells it
11if none matchboolwrites only where no live object is
12, 13, 14prefix, after, limitstr, str, uinta scan's
15, 16offset, lengthuint, uinta get's range; a length absent reads to the end

A method refuses an option it does not take, as the engine's call does. An object:

keyfieldtype
1foundboolabsent, and nothing else set, for a key that holds no live object
2keystrits path under the handle's folder
3sizeuint
4etagstrquoted, as a header carries it
5content typestr
6modifiedintunix milliseconds
7expiresintunix milliseconds; absent for an object that does not expire
8metaa map of names

A total is {1: objects, 2: bytes} and a page {1: more, 2: after}. A put that does not commit leaves nothing: a client that cancels it, a stream shorter or longer than its size, and a connection lost halfway. A get read whole is checked against the object's hash, and one whose bytes changed ends with DATA·END·ERROR and corrupt instead of its last bytes; a range is not checked.

sql

sql.open answers a handle on a database by its name, and every other call carries it. The first open of a name in the server opens it: an admin connection's applies the migrations it carries, and a data connection's finds them applied, or is refused with permission and makes no file. A database opens once a store, so every later open, and an open of a database the embedding program passed the server, checks the migrations it carries against those the file applied: one the file has not applied is permission for a data connection and in_use for an admin one, since a database applies its migrations as it opens, and one changed after it was applied is invalid. A first open without migrations is invalid; a later one may carry none.

methodrequestanswer
0x0401opena databasea handle
0x0402execa statementdone: the rows it changed and the rowid of the last it inserted
0x0403querya statementa download: the columns, a row a DATA, then {}
0x0404batchstatements one transaction runs, all or none, or with read, reads from one snapshotresults; a statement that fails fails them all, and what names it as call

A database:

keyfieldtype
1namestr[a-z0-9][a-z0-9_-]{0,63}, its file sql/<name>.db
2migrationsarray of fileseach {1: name, 2: text}: a .sql file's name, without a directory, and its text

A statement:

keyfieldtype
1handleuintabsent in a batch, whose handle is its own
2sqlstr
3argsarray of valuespositional
4nameda map of nameseach name without the :, @ or $ the statement spells it with
5writeboola query's: run on the writer, for a returning clause
6rowsboola batch statement's: answer the rows it returns rather than what it changed

An argument is nil, an integer, a float, str, bin, or a bool, which SQLite keeps as 1 or 0; a NaN is invalid, since SQLite would keep NULL. A value that comes back is one of SQLite's five, nil, an integer, a float, str or bin, bin also carrying TEXT that is not UTF-8, and it is what the row keeps: a time held as text travels as its text, whatever its column declares.

Done is {1: changes, 2: last id}, the columns {1: [name…]} and a row {1: [value…]}. Statements are {1: handle, 2: [statement…], 3: read, 4: [jobs…], 5: [kv…]} and their results {1: [result…]}, a result being done, or {3: columns, 4: [[value…]…]} for a statement that returned rows.

Each item of jobs has the shape of an enqueue's request, {1: handle, 2: [job…]}, and its queue must be opened in this database. The transaction writes the jobs after the statements, so a job commits with the rows it is about, or not at all. A job has no result. When a job fails, what names it as call, counted after the statements. A job in a view, or on a queue that lives in another file, is invalid.

Each item of kv is {1: handle, 2: [call…]}, with the calls of kv.batch: each has its method under key 0 and the fields of a KV call. Its outer handle identifies the bucket. The calls may set, delete or clear keys in a bucket opened in this database, and run after the statements and jobs. A key change has no result. A read, a set-if-absent, a change in a view, or a bucket in another file is invalid. A failure's what.call counts key changes after the statements and jobs.

A message past the agreed body is limit: a row a query downloads, or a batch's results, which travel in one message. A query holds its rows in the server's memory, 64 MiB of them at most, before the first leaves.

A data connection's statement, each of a batch's included, runs only once the check server.md describes lets it, and one it refuses is permission; it runs 30 seconds at most, past which it is limit. An admin connection's runs as it is.

sql.exec of an insert on handle 1, and its answer:

31 00 00 00  03  01  02 04  03 00 00 00                                   a body of 49 bytes, REQUEST, END, 0x0402, stream 3
83                                                                        a map of three
   01 01                                                                  handle: 1
   02 d9 24 69 6e 73 65 72 74 20 69 6e 74 6f 20 6e 6f 74 65 73 20 28 74   sql: "insert into notes (title) values (?)"
      69 74 6c 65 29 20 76 61 6c 75 65 73 20 28 3f 29
   03 91 a4 6d 69 6c 6b                                                   args: ["milk"]

05 00 00 00  04  01  00 00  03 00 00 00                                   a body of 5 bytes, RESPONSE, END, stream 3
82                                                                        a map of two
   01 01                                                                  changes: 1
   02 07                                                                  last id: 7

records

Records are one log of the store's, and each call names the streams it is about, so none opens a handle.

methodrequestanswer
0x0501appendrecords, one transaction writes all or none{}; what names a refused record as call, with its stream and name
0x0502reada querya download: {}, a record a DATA, then a page
0x0503followa cursora download: {}, a record a DATA, then the cursor after them
0x0504linesa streaman upload: another program's output as DATA, cut anywhere; {}
0x0505damaged{}the damage: {1: [damage…]}
0x0506dropa damage{}; an admin connection's alone, since it is a repair

A record:

keyfieldtype
1atintunix nanoseconds, past what a JavaScript number holds
2streamstra namespace the application names
3namestrthe event: log for a log line
4levelintslog's: -4 debug, 0 info, 4 warn, 8 error; absent for none
5bodystrabsent for none, which an empty body is not
6trace idbin16 bytes; absent for none
7span idbin8 bytes; absent for none
8contextarraywho produced it: a key, then its value's JSON, and so on
9attrsarraywhat happened, as context

["user", "42", "tags", "[\"a\"]"] is two fields, user and tags, whose values are JSON spelled as they were given, 1.2300, -0 and a big integer coming back byte for byte; keys keep their order and may repeat. A text, a stream, a name, a body, a key or a value, whose bytes are not UTF-8, as another program's output may be, travels as bin and comes back byte for byte.

A query:

keyfieldtype
1, 2from, tointunix nanoseconds, to excluded; absent for an open end
3streamsarray of strabsent or empty for every stream
4namesarray of strabsent or empty for every name
5min levelinta record without a level does not match
6trace idbin16 bytes
7, 8attrs, contextarrayfields as a record's, each one a record must hold
9newestboolnewest first; oldest first when absent
10limituintthe records a page holds: 1000 when absent, at most 10000
11budget blocksuintthe blocks one read may open; each budget narrows the server's
12budget bytesuintthe bytes it may fetch
13budget recordsuintthe records it may decode
14searchstrtext a record's body or name holds, its case ignored; 1 KiB at most

A page, a read's trailer, is {1: more, 2: from, 3: to}: more says the limit or the budget ended the page before the range did, and from and to are the range the next page reads, the query's own with one end moved past this page. A page never splits a timestamp; more records at one time than a page holds is limit.

A cursor is {1: segment, 2: row, 3: limit, 4: expired}: follow's request names the place the records it asks for begin at, {} being the oldest segment kept, and the records it takes at most; its trailer the place the next follow begins at, and the segments retention or a drop removed before the cursor reached them. A record reaches follow once its head is sealed, a segment's worth or the server's seal age, an hour unless it says otherwise, after it arrived; read sees it at once.

records.lines is {1: stream}, then the program's output in DATA frames: each line becomes a record of the stream as Go's Lines makes it, a stack trace's lines joined, a JSON or logfmt line's fields kept, its level found where its program writes it. The upload's end hands over the record the writer holds, a line cut short included, and its answer follows; a line longer than a record holds is dropped and counted in the engine's stats.

A damage is {1: stream, 2: segment, 3: head row, 4: from, 5: to, 6: reason}: a sealed segment, dropped whole, or a head row, the other absent; the times it held, unix nanoseconds; the invariant its bytes broke. A read or a follow that meets one ends with corrupt, its what naming it by the same names, segment or head row, stream, from, to and reason, and a drop of one that still reads is conflict.

records.append of one click on stream 5:

2a 00 00 00  03  01  01 05  05 00 00 00                                   a body of 42 bytes, REQUEST, END, 0x0501, stream 5
81                                                                        a map of one
   01 91                                                                  records: an array of one
      84                                                                  a map of four
         01 cf 18 d7 5b 84 2b 4e cd 15                                    at: 1,790,000,000,123,456,789 ns
         02 a3 77 65 62                                                   stream: "web"
         03 a5 63 6c 69 63 6b                                             name: "click"
         09 92 a7 65 6c 65 6d 65 6e 74 a5 22 62 75 79 22                  attrs: ["element", "\"buy\""]

metrics

Metrics are one store of series, each named by its labels, so none opens a handle.

methodrequestanswer
0x0601ingestseries and their samples, stored all or none{}; what is a refused series' labels
0x0602reada rangea download: {}, a series a DATA, then {}
0x0603aggregatea range with a width and an operationa download: {}, a series' buckets a DATA, then {}
0x0604droplabels{1: found, 2: unreadable groups}
0x0605explaina range; with an operation, the aggregate'sa plan
0x0606latesta rangea download: {}, a series' newest sample a DATA, then {}; a series without one is left out
0x0607describea description{}
0x0608describeda description of a name alonethe name's description, its unit and help empty when it has none

A description is what a metric's name means. It belongs to the name, and every series of the name shares it:

keyfieldtype
1namestrthe metric's name, its __name__
2unitstrsuch as ms, bytes or %, 32 bytes at most; absent when none
3helpstra line of help, 1024 bytes at most; absent when none

A describe with neither a unit nor help removes the name's description.

A plan is what a read, or an aggregate when the range names an operation, would spend, found in one snapshot from the series, their block directories and their heads, with no payload fetched and no sample decoded:

keyfieldtype
1seriesuintthe series it matches
2blocksuintthe blocks it opens
3summarizeduintthe blocks an aggregate answers from their summaries
4bytesuintthe bytes it fetches
5decodeduintthe samples it decodes
6 to 10limit series, blocks, bytes, decoded, answereduintthe limits it runs under
11stopsan errorthe limit it would end with, what naming it; absent when it fits

A series:

keyfieldtype
1labelsa map of namesthe series' name as __name__ among them, required on ingest; names unique, every text UTF-8
2kindstrgauge or counter
3timesbinunix milliseconds, a little-endian int64 each
4valuesbina little-endian float64 each, its bits the data: -0 and a NaN's payload come back as they went

The wire carries a series' name as the store keeps it, its label __name__; the Go API and every SDK give it apart from the labels, as name, and refuse a label of the application's beginning with __.

The two columns hold as many values each. A series longer than half a body holds comes in several DATA, one after another, each with its labels and kind, its samples going on in time order; a client joins them. Ingest keeps the value given last for a time repeated.

A range:

keyfieldtype
1matchersa map of namesthe labels a series has, exactly, its name as __name__
2, 3from, tointunix milliseconds, to excluded; both required, 2^63−1 the open end
4limit seriesuintthe series it matches; each limit narrows the server's
5limit blocksuintthe blocks it decodes
6limit bytesuintthe bytes it fetches
7limit decodeduintthe samples it decodes
8limit answereduintthe samples or buckets it answers
9widthuintaggregate's: milliseconds, the buckets starting at from
10opstraggregate's: count, sum, min, max, avg, first, last, increase or rate, a counter's alone, or delta, a gauge's
11wherearray of conditionslabels beyond equality, each label once, in the byte order of its name
12, 13by, withoutarray of straggregate's: the labels a group keeps, or all but these; an empty by is sent and joins every series of a name
14lookbackuintaggregate's: milliseconds before from where an increase, a rate or a delta looks for its first step; the width when absent

A condition:

keyfieldtype
1labelstrthe label's name
2kindstrone_of its values, none_of them, which a series without the label is too, or prefix, its one value
3valuesarray of strone at least, a thousand at most

A range finds its series by a matcher, a one_of or a prefix: a range of none_of alone is invalid, since it would scan every series. A condition of a kind the server does not have is unimplemented, what naming it, as is an operation, what naming it as op.

The server reads a range whole, within its limits, before the first series leaves, so that a slow client holds none of the engine's readers; a read or an aggregate that fails sends no series.

An aggregate's item is a series, keys 1 and 2, and its buckets as columns, as many values each:

keyfieldtype
3frombineach bucket's start, unix milliseconds, a little-endian int64 each
4tobinits end, excluded
5countbinthe samples it counted, an int64 each
6resetsbinthe resets among them, an int64 each
7valuesbinits value, a float64 computed exactly and rounded once
8flagsbina byte each: 1 when the value overflowed to an infinity, 2 when retention cut the bucket, which counted only its samples from the cutoff on, or not the step into its first, 4 when its first step started from a sample before the range

An increase, a rate and a delta count each step between samples in the bucket where it ends, the first bucket's from the last sample up to the lookback before from, so the buckets add up to the range; only a bucket holding samples is answered.

Drop removes one series and everything it holds, whether it still reads or not, the labels naming it exactly; unreadable groups counts the groups removed without the payload rows their directory no longer names.

metrics.ingest of one sample of cpu{host="web-1"} on stream 9:

3b 00 00 00  03  01  01 06  09 00 00 00                                   a body of 59 bytes, REQUEST, END, 0x0601, stream 9
81                                                                        a map of one
   01 91                                                                  series: an array of one
      84                                                                  a map of four
         01 82                                                            labels: a map of two names
            a8 5f 5f 6e 61 6d 65 5f 5f a3 63 70 75                        __name__: "cpu"
            a4 68 6f 73 74 a5 77 65 62 2d 31                              host: "web-1"
         02 a5 67 61 75 67 65                                             kind: "gauge"
         03 c4 08 00 6c 50 c4 a0 01 00 00                                 times: [1,790,000,000,000]
         04 c4 08 00 00 00 00 00 00 e0 3f                                 values: [0.5]

An index opens a handle, as a bucket does; its documents are JSON as str, and a vector is bin, its floats little-endian, four bytes a dimension. protocol/search.wire holds every message.

methodrequestanswer
0x0b01opena name and the index's settingsa handle; a read connection's opens an index only as it is
0x0b02seta handle, an id, a document, a vector or none{}
0x0b03setManya handle and documents, all set or none{}; what names a refused one's place and id
0x0b04deletea handle and an id{1: deleted}
0x0b05geta handle and an id{1: found, 2: the document}
0x0b06searcha handle, text, a vector, a where, a page{1: hits, 2: corrected}, a hit {1: id, 2: document, 3: score}
0x0b07statusa handle{1: ready, rebuilding or failed, 2: done, 3: error}
0x0b08dropIndexa name{1: dropped}
0x0b09list{}the indexes: name, documents, vectors, opened, status

A set on an index with vectors gives vector, or noVector: true for a document only its words find; one that says neither is invalid.

A table's index opens with database, a handle sql.open gave, and table, with key and vectorColumn where it needs them: its fields and filters are columns of the table. Its search answers columns once and each hit's row, a cell a column, as sql.Rows carries rows, and value null. A set, setMany, delete or get on it is invalid, since the table owns the rows, and dropIndex names the database beside the name.

search's where

A search's where is a JSON list of conditions, every one of which a document passes, as the Bun SDK writes its where object:

conditionpasses a document that
{"eq": [path, value]}holds the value, or a list holding it
{"in": [path, [values]]}holds one of them; an empty list passes none
{"missing": path}holds no value there: no field, null or an empty list
{"range": [path, {"gte": 10, "lt": 100}]}holds a number within it: gt, gte, lt and lte
{"or": [conditions]}, {"and": [conditions]}passes any, or every, of them

A value is text, a number or a boolean: the text "50" never passes for the number 50, and an integer past 2⁵³ is written as its digits and matched exactly. A path the index was not opened with is invalid.

Edit this page on GitHub Updated 19 October 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results