Authoritative source: TachyonDB PROTOCOL.md. Two builds speak the same wire: tachyon8 (ARM64) and tachyond (x64). Page put/get tests are green on Snapdragon.
| Area | Status | Notes |
|---|---|---|
| Wire framing | Live | Length‑prefixed frames; handshake magic TYDB; little‑endian |
| BQL bytecode client payload | Live | Raw opcodes — no SQL/JSON text on the wire |
| Core KV + filter ops | Live | GET / PUT / ALL / GT / LT / EQU / ORDER_BY / LIMIT / OFFSET / AND / OR / NOT |
| Stack immediates | Live | PUSH_I32 / PUSH_I64 / PUSH_F64 / PUSH_STR / LOAD_CONST_0..15 / POP / DUP |
| Schema ops | Live | CREATE_TYPE / DROP_TYPE / KEY / TYPE / FIELD / DELETE / COUNT |
| Txn fences | Fence | BEGIN / COMMIT / ROLLBACK are no‑op fences (single‑threaded server) |
| AUTH / STATUS / PING / END | Live | AUTH is FNV placeholder — not cryptographic (see honesty note) |
| Pages (URLChannel / BML bodies) | Live | PAGE_PUT (0x33) / PAGE_GET (0x34) as DATA_BYTE_ARRAY by path |
| EXT space | Partial | JOIN / SPLIT / REINDEX / COMPRESS / FLUSH / SHUTDOWN codes defined; FLUSH drives snapshot |
| Persistence | Snapshot | snapshot.tdb authoritative; journal is marker‑only (no rich replay yet) |
| Listen bind | Localhost | Default 127.0.0.1 — do not bind 0.0.0.0 until AUTH is cryptographic |
Current AUTH hashes with FNV‑1a64 over salt "Tachyon!" || password. Default credentials: admin / tachyon. This is a placeholder, not a security boundary. Cryptographic AUTH (Ed25519 / AES direction already present in Channels) is roadmap item #1.
Unlike SQL or JSON query languages that rely on text parsing, tokenization, and AST generation, BQL encodes operations as binary bytecodes. Common commands occupy just 1 byte (0x00–0xFE). There is no runtime lexical analysis — the byte stream is executed directly via a 256‑entry jump table.
// SQL (text): dozens of bytes, needs parsing
"SELECT name FROM players WHERE health > 50"
// BQL: push value, set field, compare, scan — raw bytes
PUSH_I32 50 · FIELD health · GT · ALL · GET name · END
// No lexer. No keyword table. Opcode → handler.
Why it matters: cycles spent on string comparisons, keyword recognition, and grammar validation disappear. BQL opcodes map directly to native handlers (jmp [op_table + rax*8] on the hot path).
u32le length (= 12) + payload u32le magic='TYDB' (0x42445954), u8 version=1, u8 pad[3], u32le features=0.u32le length + payload.u32le status · u32le row_count · repeated ChannelData cells
[u8 type][u32le len][len bytes].Little‑endian throughout. Single‑threaded server; many concurrent clients via select().
| Code | Name | Code | Name |
|---|---|---|---|
| 0 | OK | 7 | OOM |
| 1 | AUTH | 8 | IO |
| 2 | BAD_OPCODE | 9 | PROTOCOL |
| 3 | STACK | 10 | DENIED |
| 4 | NOT_FOUND | 11 | TXN |
| 5 | EXISTS | 12 | INTERNAL |
| 6 | SCHEMA | 13 | TOO_LARGE |
Default branch width = 24 → 16,777,216 buckets. Bucket entry = 4‑byte arena offset → 64 MiB table (offsets address a 4 GiB arena; collision chains live in the arena). Integer keys use the low 24 bits; other keys fold with FNV‑1a to 24 bits.
Every opcode triggers a precompiled handler. Dispatch is a 256‑entry jump table — no string switch.
| Opcode | Mnemonic | Stack / immediates |
|---|---|---|
| 0x00 | NOP | — |
| 0x01 | GET | [field_id:u8] → value; emits cell |
| 0x02 | PUT | value →; writes keyed record |
| 0x03 | ALL | scan type with optional filter; emits rows |
| 0x04 | GT | filter: FIELD f; PUSH v; GT |
| 0x05 | LT | same pattern |
| 0x06 | EQU | same pattern |
| 0x07 | ORDER_BY1 | [field_id:u8] |
| 0x08 | ORDER_BY2 | [f1][f2] |
| 0x09 | LIMIT | int → |
| 0x0A | OFFSET | int → |
| 0x0B | AND | (bool, bool) → bool |
| 0x0C | OR | (bool, bool) → bool |
| 0x0D | NOT | bool → bool |
| 0x0E | PUSH_I32 | [i32le] → int |
| 0x0F | PUSH_I64 | [i64le] → long |
| 0x10–0x1F | LOAD_CONST_0..15 | → int (immediate small constants) |
| 0x20 | PUSH_STR | [u16le len][bytes] → CHAR_ARRAY cell ptr |
| 0x21 | POP | x → |
| 0x22 | DUP | x → x x |
| 0x23 | CREATE_TYPE | [nlen][name][nfields]{[flen][name][cd]}* → type_id |
| 0x24 | DROP_TYPE | [type_id] |
| 0x25 | DELETE | keyed delete |
| 0x26 | COUNT | → int |
| 0x27 | BEGIN | txn fence (no‑op; single‑threaded) |
| 0x28 | COMMIT | txn fence |
| 0x29 | ROLLBACK | txn fence |
| 0x2A | AUTH | [ulen][user][plen][pass] — FNV placeholder |
| 0x2B | STATUS | emits uptime, queries, records, buckets_used |
| 0x2C | PING | → 1 |
| 0x2D | END | halt OK |
| 0x2E | KEY | str → sets current key |
| 0x2F | TYPE | [type_id:u8] |
| 0x30 | FIELD | [field_id:u8] (0xFF = whole blob PUT) |
| 0x31 | PUSH_F64 | [f64le] |
| 0x33 | PAGE_PUT | [u16le path_len][path][u32le body_len][body] |
| 0x34 | PAGE_GET | [u16le path_len][path] |
| 0xFE | EXT | [u16le code] — extended dispatch |
// Intent: GET field where value > 50, then END
// SQL text: "SELECT name FROM players WHERE health > 50" → ~40+ bytes of ASCII
// BQL bytes: PUSH_I32 + FIELD + GT + ALL/GET pattern + END → a handful of bytes
// Exact length depends on field/type encoding — the win is zero parsing + tiny wire.
Rare / admin operations stay off the hot 1‑byte path. Prefix with OP_EXT (0xFE), then a little‑endian 16‑bit code.
| Code | Mnemonic | Use |
|---|---|---|
| 0x0001 | JOIN | Cross‑record relation |
| 0x0002 | SPLIT | Break a prior join |
| 0x0003 | REINDEX | Rebuild indexes |
| 0x0004 | COMPRESS | Compression trigger |
| 0x0005 | FLUSH | Write snapshot (snapshot.tdb) |
| 0x0006 | SHUTDOWN | Graceful server stop |
// FLUSH snapshot — 3 bytes
[0xFE] [0x05] [0x00] // OP_EXT + 0x0005 little-endian
// JOIN — 3 bytes + args
[0xFE] [0x01] [0x00] // OP_EXT + 0x0001
Tradeoff: keeping rare ops in the 16‑bit escape leaves the common case lean without capping future commands.
Page bodies are stored as DATA_BYTE_ARRAY (type 8) keyed by a UTF‑8 path. Internal type id for page records is 0xFE (not a wire ChannelData code).
| Op | Name | Request | Response |
|---|---|---|---|
| 0x33 | PAGE_PUT | [u16le path_len][path][u32le body_len][body] |
status; row_count=0 |
| 0x34 | PAGE_GET | [u16le path_len][path] |
status; one DATA_BYTE_ARRAY cell, or NOT_FOUND (4) |
PAGE_PUT path + body → status 0PAGE_GET same path → status 0, cell type=8, bytes identicalNOT_FOUND), no cellEXT FLUSH then restart → same PAGE_GET still returns the body (snapshot)Empty bodies (body_len=0) are valid. Tested sizes include 0, 100, 16348, 16349, 70000, and 1 MiB. Bodies larger than max_page (config default 16 MiB; clamped 64 KiB..64 MiB) return status 13 (TOO_LARGE). Per‑connection I/O buffers start at 8 KiB and grow on demand. Frames prefer one TCP segment (TCP_NODELAY).
Aligned with channels7 defines.h. On the Tachyon wire:
0..31 (do not remap)Do not treat bit 6 as signed or bit 7 as string‑compress on the Tachyon wire. Those flags belonged to an older Channel packing sketch and are wrong for this protocol.
Bits: 7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+
| memory kind | type code 0..31 |
+-----+-----+-----+-----+-----+-----+-----+-----+
| Code | Name | Code | Name |
|---|---|---|---|
| 0 | DATA_BYTE | 8 | DATA_BYTE_ARRAY |
| 1 | DATA_SHORT | 9–15 | *_ARRAY |
| 2 | DATA_CHAR (16‑bit) | 16–23 | *_2D_ARRAY |
| 3 | DATA_INT | 24–31 | *_3D_ARRAY |
| 4 | DATA_LONG | ||
| 5 | DATA_FLOAT | ||
| 6 | DATA_DOUBLE | ||
| 7 | DATA_OBJECT |
BML / page bodies use DATA_BYTE_ARRAY (8) keyed by path. Zero string conversion on the hot path — the type byte selects the handler.
SQL sends human‑readable keywords as ASCII/UTF‑8. BQL replaces each keyword with a 1‑byte opcode. Result: much smaller wire payloads and zero parsing cost on the server.
| Query intent | SQL (illustrative) | BQL shape | Why it wins |
|---|---|---|---|
| Fetch one field | ~20–30 bytes of text | GET + field_id (+ TYPE/KEY as needed) | No tokenizer |
| Filter + AND | “WHERE … AND …” prose | FIELD · PUSH · GT/LT/EQU · AND | Compare ops are 1 byte each |
| Order + limit | “ORDER BY … LIMIT …” | ORDER_BY1 · LIMIT | Pagination without keyword bloat |
| Store a page body | REST/JSON multipart or SQL BLOB dance | PAGE_PUT path + raw bytes | Direct binary cell |
BQL eliminates string scanning on the query path. Cost is opcode dispatch + native handlers — not µs tables invented for marketing.
Qualitative wins that matter in practice: smaller frames on constrained links, predictable latency (no planner surprises), and no SQL‑injection surface because there is no SQL string to inject into.
Real numbers only · 25 September 2026 · DESKTOP-2QI0DQ0 · Snapdragon 7c Gen 2 @ 2.55 GHz · Windows on ARM · native ARM64 tachyon8 + benches (not x64 emulation).
Wire / server only Cont, URLChannel, and the Display sit above these numbers — do not quote this table as end‑to‑end Cont throughput.
Fair page/BLOB store comparison: Tachyon = persistent TCP loopback + BQL PAGE_* on 127.0.0.1:7447. SQLite = in‑process library (file WAL or :memory:) — no socket. This is not a SQL‑parse contest and not the same IPC.
n=2000 (1 MiB: n=200); 50 warm‑up ops discarded; SQLite 3.42.0, journal_mode=WAL, synchronous=NORMAL.
| Side | Op | Size | ops/sec | p50 | p99 |
|---|---|---|---|---|---|
| Tachyon8 TCP | PAGE_GET | 16 B | 2197 | 0.344 ms | 1.206 ms |
| Tachyon8 TCP | PAGE_PUT | 16 B | 2151 | 0.288 ms | 1.232 ms |
| Tachyon8 TCP | PAGE_GET | 4 KiB | 2217 | 0.297 ms | 1.225 ms |
| Tachyon8 TCP | PAGE_PUT | 4 KiB | 2020 | 0.323 ms | 1.360 ms |
| Tachyon8 TCP | PAGE_GET | 1 MiB | 198 | 4.161 ms | 9.757 ms |
| SQLite file WAL | SELECT BLOB | 16 B | 94066 | 0.006 ms | 0.037 ms |
| SQLite file WAL | INSERT OR REPLACE | 16 B | 2908 | 0.066 ms | 1.459 ms |
| SQLite file WAL | SELECT BLOB | 4 KiB | 34899 | 0.009 ms | 0.074 ms |
| SQLite file WAL | INSERT OR REPLACE | 4 KiB | 1680 | 0.105 ms | 1.568 ms |
| SQLite file WAL | SELECT BLOB | 1 MiB | 515 | 0.907 ms | 8.509 ms |
SQLite SELECT wins on latency because there is no round‑trip. On PUT‑ish work, Tachyon PAGE_PUT 4 KiB (2020 ops/s) beat file SQLite INSERT 4 KiB (1680 ops/s) on this run — arena + TCP vs WAL/disk. Payload compactness vs SQL strings is a separate win (opcodes, not keywords).
Earlier Linux‑native loopback baseline (different CPU): PAGE_GET 4 KiB ≈ 5307 ops/s, p50 0.059 ms — Snapdragon 7c Gen 2 is slower as expected. Full raw log kept with the publish tree.
The BQL VM uses a static array of handlers. Fetching an opcode becomes a single indexed jump. On x64/ARM64 assembler builds this is literally jmp [op_table + rax*8]. The table fits in a few KB and stays L1‑friendly.
typedef void (*bql_handler)(VMState*);
static const bql_handler dispatch[256] = {
[0x00] = op_nop,
[0x01] = op_get,
[0x02] = op_put,
[0x03] = op_all,
[0x33] = op_page_put,
[0x34] = op_page_get,
[0xFE] = op_ext,
// ...
};
while (!vm_halt) {
uint8_t opcode = *vm->ip++;
dispatch[opcode](vm);
}
Not interpreted in the text sense: there is no lexer, no grammar walk, no keyword strcmp. It is threaded binary code with a fixed opcode → handler map.
BQL is not a drop‑in SQL replacement for ad‑hoc analytics. It shines as the machine query language for TachyonDB / Channels / Cont — high‑throughput page stores, keyed records, and binary pipelines where parsing tax and payload bloat hurt. For analyst SQL, use SQL. For your stack’s hot path, BQL is the right shape.
Priority order for Cont / URLChannel / TachyonDB hardening. Each item earns its bit cost — frequency packing still rules.
Replace FNV placeholder with real crypto already in the Channels direction (Ed25519 / AES, in‑tree). Only after this is solid should the server bind beyond 127.0.0.1.
Snapshot‑only durability works; marker‑only journal does not. Replayable journal = crash recovery without rewriting the whole image every time.
Hygiene for Cont / URLChannel: remove stale paths, list what is stored, and report size/mtime without a full GET.
These are frequent enough to deserve single‑byte opcodes — same packing rule that kept GET/PUT lean.
Beyond the default 24‑bit primary buckets — either EXT codes or new 1‑byte ops once the access pattern is proven.
Fully exercise CREATE_TYPE + GET/PUT fields with a tiny encoder library so humans never hand‑hex bytecode.
Huge bodies without forcing a single monolithic frame up to max_page.
After crypto AUTH: who may read/write which key space.
Cont multi‑machine: replicate pages and keyed records without inventing a second protocol.
Closes the debugging gap the old docs already called out — make bytecode readable again for humans.
BQL embraces that machines speak binary, not English. One‑byte opcodes for the common case, 16‑bit EXT for the rare case, ChannelData packed so type selection is a branch — not a string compare.