Changelog

Notable changes to SoliDB, newest first. For the full commit-level history see CHANGELOG.md in the repository.

Unreleased

No unreleased changes yet.

v1.0.1

September 1, 2026
  • solidb-dump --all-databases — dump every database the credentials can read into one stream. Every record already carried its own _database, so the combined dump restores to the right place with no -d; what was missing was a way to ask for all of them without scripting a loop over GET /_api/databases. That endpoint filters by permission, so this captures exactly the databases the principal may read — _system included, with its credential collections skipped as everywhere else. Conflicts with -d and -c, and a database that cannot be listed aborts the dump rather than leaving a silently partial file.
  • solidb-restore --exclude-collection — skip collections on the way in. Repeatable, comma-separated values accepted, and * matches any run of characters (--exclude-collection 'events_*'). Matched against the collection name in the dump, so it still selects the right records when -c is rewriting the target. Every record type is filtered — documents, the collection and index declarations, columnar rows and blob chunks — so an excluded collection is not created at all. Excluded records are counted and named in the summary and do not affect the exit status; a pattern that matches nothing warns rather than reporting a clean run. Pairs with --all-databases: --exclude-collection '_*' restores the user data out of a whole-instance dump without replaying _system bookkeeping into a live server.
  • Fixed: the driver protocol's list_collections still returned _env — it handed back the raw column-family list, so credential collections appeared over the binary protocol while the HTTP listing hid them. Names only, and every driver read path already refused them, but the two listings disagreed and listing _env discloses that a database holds provider keys.
  • Fixed: a database with an _env collection could not list its collectionsGET /_api/database/{db}/collection enumerates column families, so _env appeared in the listing for any database that had ever had an env var set. The credential guard added in 1.0.0 then refused to open it, and the listing handler propagated that as a 403 for the whole request — for every principal, admin included. Setting one env var, or opening the admin Env page (which creates the collection), was enough to make the database's collection list unreachable. Credential collections are now skipped in the listing, keyed off the same protected-collection list the guard uses so the two cannot drift. _env is no longer listed at all: it was never readable through this API, and the listing publishes a document count and storage stats per entry. The admin-only /_api/database/{db}/env endpoints are unaffected.

v1.0.0

August 31, 2026

Security hardening, SDBQL function completeness, and one product breaking change: the client-facing job and cron queue is removed. Application background jobs now live in the Soli framework. Also in this release, the memory and idle-CPU work described in the last three entries below. Triggers keep working, on the same machinery as before. Also breaking if you relied on them: default bind is loopback, replication TCP requires a keyfile, LENGTH on a string is character count (not a collection count), installing Lua requires Admin, and API keys must declare a role.

  • New SDBQL syntax: set operations, recursive CTEs, RETURN DISTINCT, COLLECT … KEEP, NONE, standalone OFFSET — combine query blocks with UNION [ALL], INTERSECT and EXCEPT (either side may be parenthesized; duplicates removed except for UNION ALL; chains follow SQL precedence, so INTERSECT binds tighter than UNION/EXCEPT). WITH RECURSIVE name AS (<anchor> UNION ALL <step>) iterates until the step stops producing rows, binding the CTE name inside the step to the rows of the previous iteration (hierarchies, org charts, transitive closures), capped at 1,000 iterations / 1M rows. RETURN DISTINCT expr deduplicates rows. COLLECT … INTO g KEEP v1, v2 restricts which variables land in the group arrays. NONE(x IN arr SATISFIES cond) and NONE(arr, x -> cond) are true when nothing satisfies. OFFSET n works alone or as LIMIT n OFFSET m.
  • Native HTTPS termination — new --tls-cert / --tls-key flags terminate TLS in the server itself (rustls, no OpenSSL). On the multiplexed port the listener sniffs for a TLS handshake and terminates only when the client offers one, so HTTP and the native driver protocol run inside the tunnel for TLS-capable clients while the plaintext-only driver SDKs and sync/cluster peers keep connecting; SOLIDB_TLS_REQUIRE=1 refuses plaintext there. Both flags are required together.
  • Per-client API rate limiting — only /auth/login was throttled before; now the whole router has a per-client-IP sliding-window limiter (default 600 requests / 60s) answering 429 with Retry-After before any handler work. Internal cluster traffic and CORS preflights are exempt. Configure with SOLIDB_API_RATE_LIMIT (0 disables) and SOLIDB_API_RATE_WINDOW_SECS.
  • Driver queries have a timeout — HTTP capped execution at 30s but the binary protocol ran queries unbounded on a runtime thread. Driver Query and Explain now run under the same 30s cap, on the same long-running-query gate HTTP uses so point reads stay inline.
  • Tokens in ?token= are restricted to WebSocket endpoints — the query-string branch is accepted for exactly /_api/ws/changefeed, /_api/cluster/status/ws and /_api/monitoring/ws (browser WebSockets cannot send headers) and refused everywhere else — including the REST endpoint that issues those tokens.
  • cargo-fuzz targets — fuzz harnesses for the SDBQL parser, lexer, driver MessagePack decoding, and restore JSONL parsing live in fuzz/.
  • Replication TCP fails closed without a keyfile — the HTTP cluster bus already required a secret; the multiplexed sync socket still skipped HMAC when no keyfile existed. Unauthenticated replication is refused unless SOLIDB_ALLOW_UNAUTHENTICATED_SYNC=true (local tests only).
  • HTTP listeners default to loopback — bind is 127.0.0.1 unless --host or SOLIDB_HOST says otherwise. Use 0.0.0.0 only when something in front of the process terminates TLS.
  • Query-driven auto-indexes (opt-in) — collection autoIndex: true, or unset plus SOLIDB_AUTO_INDEX=1. A FOR + FILTER miss creates a persistent _auto_{field} index (cap 16) only for a caller holding Write or Admin — on every query route, and never without a principal. Explicit false overrides the env var. SORT, null filters, sharded collections, collections above SOLIDB_AUTO_INDEX_MAX_DOCS (default 1,000,000), and fields no document carries are not auto-indexed. EXPLAIN reports a candidate and does not create.
  • The binary protocol carries the session principal — driver queries now run with the same identity HTTP queries do, so CURRENT_USER, CURRENT_ROLES and row policies apply there as well. A row policy that previously only filtered HTTP reads now also filters driver reads.
  • A failed index backfill no longer leaves a half-built indexcreate_index wrote its metadata before filling the entries, so an interrupted build (disk full, shutdown) left an index that later queries used and silently under-returned from. The metadata is now rolled back, and the backfill streams the collection instead of holding every decoded document in memory.
  • --no-lua / SOLIDB_NO_LUA=1 skips the Lua VM pool — custom scripts, /api/{db}/{service}/…, and the REPL return 501; a trigger whose action is a Lua script fails its job immediately, without retries. Script documents can still be stored. Use this on nodes that never run Lua to drop the idle RSS of the pre-warmed VMs (at least four states).
  • Installing Lua is Admin — creating or changing _scripts / _services required only Write, so a collection editor could publish unauthenticated /api/{db}/{service}/… handlers. Those mutations now need Admin. New services default to require_auth: true. solidb.env secrets are injected only for authenticated admin scripts.
  • Cluster control-plane HTTP is Adminremove-node, rebalance, sync-log prune/stats, cluster info/status, blob rebalance, and the cluster status WebSocket accepted any authenticated principal, including viewer. They now require Admin.
  • Livequery JWTs are path-restricted on ?token= as well as Bearer. JWT roles for real _admins users are reloaded on each request. API keys must declare at least one role (empty no longer defaults to admin).
  • /metrics requires authentication unless SOLIDB_METRICS_PUBLIC=1. Present SOLIDB_METRICS_TOKEN or an admin JWT.
  • Physical backups are jailed under SOLIDB_BACKUP_ROOT (or {data_dir}/backups). Webhook URLs are SSRF-checked. SOLIDB_DB_AUTHZ_MODE=warn and SOLIDB_LUA_FAST_MODE are ignored without their explicit unsafe companion flags.
  • Passwords must be at least 12 characters. The admin app no longer falls back to admin/admin. Lua crypto.jwt_decode compares signatures in constant time.
  • SDBQL string functions are AQL-shaped and Unicode-correct — offsets and LENGTH on strings are Unicode scalar counts (BYTE_LENGTH is UTF-8 bytes). Added LIKE() as a function, REGEX_MATCHES, REGEX_SPLIT, REPEAT, LPAD/RPAD, JOIN, MASK, WORD_COUNT, TRUNCATE_TEXT, RANDOM_TOKEN. ENCODE_URI percent-encodes UTF-8. Null string arguments propagate as null. Regexes are compiled through safe_regex and cached. REPEAT / pad results are capped at 1 MiB.
  • SDBQL array, math, and object functions the reference already listed now existTAKE, DROP, CHUNK, ZIP, CONTAINS on arrays; MOD, CLAMP, variadic MIN/MAX; GET, DEEP_MERGE, ENTRIES, FROM_ENTRIES, JSON_POINTER. NTH accepts a negative index. VAR_SAMP / STDDEV_SAMP divide by n-1. RANGE refuses more than 1M elements. SHIFT([]) no longer panics.
  • SDBQL function dispatch is prefix-routedUPPER no longer walks the DATE_* / SQRT match arms. Added BIT_AND/BIT_OR/BIT_XOR/BIT_NEGATE/BIT_SHIFT_*, OUTERSECTION, IS_DATE, IS_KEY, GEO_EQUALS. COUNT on an object is the key count. TO_NUMBER(null) is null.
  • SDBQL date functions share one parserYYYY-MM-DD and second-resolution epochs parse. DATE_ADD uses calendar months (31 Jan + 1 month → 28/29 Feb). Added DATE_COMPARE, DATE_LEAPYEAR, DATE_MILLISECOND, DATE_ISOWEEKYEAR. DATE_DIFF unit is optional. DATE_TRUNC accepts week. Null date arguments propagate.
  • SDBQL set and slice helpers no longer walk the array twiceUNIQUE, UNION, INTERSECTION, MINUS and COUNT_DISTINCT hash values (collision-checked) instead of serde_json::to_string or Vec::contains. APPEND / UNSHIFT / FLATTEN reserve; SHIFT copies the tail instead of remove(0). SORTED uses sort_unstable_by. KEEP / UNSET copy only surviving keys. ASCII SUBSTRING slices bytes.
  • SDBQL graph, search, GeoJSON, and identity helpers — Dijkstra on SHORTEST_PATH OPTIONS { weight }; ALL_SHORTEST_PATHS, K_SHORTEST_PATHS (Yen), K_PATHS; CREATE_GRAPH/GRAPH_INFO catalog; GRAPH name resolves the first edge collection; MATCH (a:coll {_key: k})-[:e*1..n]->(b). CREATE_VIEW search aliases; SEARCH scored filter; SEARCH_INDEX fulltext; TOKENS/PHRASE/BOOST. GeoJSON constructors plus GEO_CONTAINS/GEO_INTERSECTS/GEO_IN_RANGE/GEO_AREA. PARSE_IDENTIFIER, recursive keep/unset, ZIP object form, ZIP_OBJECT, DATE_ROUND, APPLY/CALL, MINHASH. CAN(action, doc) uses _acl/owner. Batch and transactional writes are versioned. VALID_TIME AS OF filters valid_from/valid_to.
  • SDBQL time-series, sketches, semantic operators, and HOFsMAP/FILTER/FLAT_MAP/GROUP_BY/SORT_BY/WINDOW_BY take lambdas without |>. <=> is vector cosine distance or a three-way compare; binary ~ is trigram match. Added DELTA, RATE, FILL, RESAMPLE, ASOF JOIN, HyperLogLog / percentile / top-k sketches, MATCH_SEQ, REDACT. Graph FOR v, e, p plus PRUNE. SYSTEM_TIME AS OF and SNAPSHOT_DIFF on versioned collections. CURRENT_USER / CAN / ROW_POLICY. EMBED / EXTRACT / CITE / GROUNDED.
  • LENGTH("users") is no longer a collection count — if the string happened to be a collection name, LENGTH returned the document count instead of the character length. Use COLLECTION_COUNT("users") or LENGTH((FOR d IN users RETURN 1)).
  • Removed: the queue and cron API — ten HTTP endpoints (GET/PUT /_api/database/{db}/queues…, …/queues/{name}/jobs, …/queues/{name}/enqueue, DELETE …/queues/jobs/{id}, …/run-now, and all four /cron routes); eight driver-protocol commands (list_queues, list_jobs, enqueue_job, cancel_job, and the four cron commands); the Lua global db:enqueue(queue, script, params, options); and the Rust items queue::{CronJob, QueueConfig} and QueueWorker::check_cron_jobs. Per-queue pause and concurrency settings go with them — they were reachable only through the removed endpoint, so _queue_config can never be written again.
  • Removed: the client and UI surfaces — the jobs and cron sub-clients in the JavaScript, Ruby, PHP, Python, Go and Elixir SDKs, and the queue command variants in the Rust client. The admin UI loses its Queues and Cron pages; the CLI TUI loses its Jobs tab, so tab 5 is now Cluster. The Queues and Jobs API documentation pages are gone, and the framework job documentation mirrored here now describes Soli's in-process engine.
  • Kept: trigger dispatch — a trigger still fires by inserting a row into _jobs, and the worker still claims that row and runs its Lua script or posts its signed webhook. The claim loop, the executors, the HMAC signing, the target validators and the Job / JobStatus types all remain, as does the notifier a trigger uses to wake the worker. Embedding generation and materialized-view refresh are untouched: they share the worker loop but use none of the job types. QUEUE_WORKERS now sizes that maintenance worker rather than an application queue.
  • Action required — if you called any endpoint, driver command, SDK sub-client or db:enqueue above, move that work to Soli's job engine. Let the queue drain before upgrading: a pending row that no trigger created will never run. Stored _cron_jobs and _queue_config collections become orphan data — nothing reads them, and a cron schedule you configured stops firing with no error. Drop both collections when you are ready.
  • Freed memory goes back to the OS — jemalloc was linked but never configured, and its background_thread defaults to off. Without it an arena only purges its dirty pages while some thread keeps allocating in that arena, so every Tokio worker that handled a burst of concurrent queries grew an arena and then went idle holding it. Measured: 3200 queries at 32× concurrency took RSS from 662 MB to 1296 MB and it was still 954 MB five minutes later. With a background purger and a 2s decay the same burst comes back to 625 MB, and the idle baseline drops ~18%. The settings are applied through a link-time symbol, which fails silently if it ever stops matching what the allocator looks for — so the effective values are read back and logged at startup, as a warning when the tuning did not take.
  • Idle collections cost nothing to monitor — the cluster stats collector and the heartbeat each walked every collection in every database every 5 seconds. The collector deleted and reinserted a document per collection (~3400 writes a cycle, ~690/s) and the heartbeat read ten RocksDB properties per collection (~17k lookups a cycle), producing results identical to the previous cycle nearly every time; together they accounted for 14 MB/s of allocator churn on an otherwise idle server. Both now gate on the collection's cached document and blob-chunk counters — plain atomics, no I/O — and recompute only what moved, with a five-minute forced refresh so size drift from a compaction still lands. The collector additionally skips writing a document identical to the one already stored, and the heartbeat skips the walk outright on a node with no peers, since those figures are only ever sent to peers. Listing collections for a sweep is also one pass over the column families rather than one per database, which on the instance above was 153k string clones a sweep.
  • Storage memory has knobs that can be set one at a time — three RocksDB defaults the prod profile keeps for throughput have no ceiling, and each is silent until the instance is large enough to hurt: with db_write_buffer_size unset, total memtable RAM is the number of write-active collections times 64 MB with nothing forcing an early flush; with max_open_files at -1 no SST is ever closed; and with cache_index_and_filter_blocks off, each open SST pins its index and filter blocks outside the 512 MB block cache, so that memory grows with the dataset and is never evicted. Until now the only way to bound any of them was --dev, which also cuts the block cache to 128 MB and background jobs to 2 — a throughput cliff nobody wants in production. Each knob is now a flag of its own (--memtable-budget, --max-open-files, --bounded-index-cache, --block-cache, --write-buffer-size, --max-background-jobs), each with a SOLIDB_* environment variable, applied on top of whichever preset is in force; --dev stays exactly what it was. Sizes are written the way the engine writes them (512MB, 2GB), and a typo stops the node rather than quietly configuring a different amount of memory. The effective values are logged at startup, with a warning naming the flag to set whenever the memtable budget is unlimited or index blocks are pinned against an unlimited table cache — the point being that these were previously invisible choices.
  • API rate limiting is off unless asked for, and keyed on the caller rather than the address — two changes to the limiter added earlier in this release. It is now opt-in: SOLIDB_API_RATE_LIMIT defaults to 0. A database usually sits behind an application tier it trusts, and throttling that tier turns a capacity problem into an availability one — the caller gets a 429 instead of waiting. Whoever exposes a node to untrusted clients knows they have, and turns it on. Second, when it is on the budget is keyed on the request's credential where there is one, and on the address only otherwise. Keying on the address alone divides a single bucket between every caller sharing it — several applications on one host, or every client behind a reverse proxy without SOLIDB_TRUST_PROXY_HEADERS=1 — so each can sit well inside the budget while the total goes past it and all of them start seeing 429s. Measured on a box running eight framework apps against one node: ~19 req/s combined against a budget of 10 req/s. The credential is hashed, never stored, and deliberately not verified here — verifying it would authenticate every request twice — so SOLIDB_API_RATE_LIMIT_PER_IP (default 10× the budget) still caps the address, which is what stops a caller minting a credential per request to get a fresh bucket each time.

v0.34.0

August 5, 2026

Mostly a clustering release. Multi-machine replication could not work at all, and each fault hid the next — so every fix below was found by deploying two real nodes rather than by a test. Two changes can stop a node that used to start: the replication bus now requires a shared secret, and an unroutable --advertise address is refused when peers are configured. That is why this is a minor release rather than a patch — read those two entries before upgrading a running cluster.

  • Credential collections are no longer readable at Read_env is where SoliDB tells you to put provider API keys, but it was an ordinary collection: any principal with Read on the database could dump every key through GET /_api/database/{db}/env, the document API, SDBQL (FOR d IN _env), or the driver protocol. The same held for _admins (argon2 password hashes) and _api_keys. These three are now refused by every path that takes a caller-supplied collection name, and the /env endpoints require Admin on the database — which is what the documentation already promised. Server-side readers (the LLM client, authentication, the Lua solidb.env binding) are unaffected. Because credentials are no longer reachable through the query API, solidb-dump skips them and says so; capture them with a physical backup (POST /_api/backup).
  • The cluster bus fails closed instead of open — messages were signed and verified when a secret was configured; with none, both arms fell through and sent unsigned / accepted unverified. So a cluster started without a keyfile had no authentication on its replication bus and looked identical to one that did. The asymmetry was the dangerous part: a node with a secret rejects unsigned messages, a node without one accepts both, so one misconfigured member silently downgraded what it would take from anyone. A shared secret is now required. Action required: a cluster without a keyfile will not start — see the cluster documentation for generating one.
  • A cluster can replicate across machines — the advertised replication address was hardcoded to 127.0.0.1, with no flag and no override, so every node told every peer to reach it at an address each peer reads as itself: a multi-machine cluster could not replicate, structurally, while logging nothing unusual. New --advertise flag, defaulting to --host. Action required: with peers configured, an unroutable value (loopback, 0.0.0.0, ::, localhost) is now refused at startup and the error names both ways to fix it — a cluster that silently cannot replicate is worse than one that will not start. Loopback is refused only when at least one peer is elsewhere, so two nodes on one host still work. A hostname is accepted without resolving: resolving here would let a DNS answer decide whether a node may start, and that answer can differ from the one a peer gets.
  • A fresh cluster gets an admin account — “am I joining an existing cluster” was inferred from “peers is not empty”, so in a cluster whose members list each other, every node skipped creating _admins — the first one included. Nothing created an admin, nothing errored, and the cluster answered 401 to everything with four INFO lines to explain it. The database cannot distinguish the two cases from a peer list, so the skip is now a WARN naming both ways out: start the first node with no --peer, or set SOLIDB_ADMIN_PASSWORD.
  • A joining node can ask for the data that predates it_admins never reached a node that joined, so the peer answered 401 forever and an application could only ever point at the seed. Three faults stacked: the sync worker's command sender was discarded at construction (let (_tx, rx) = …), so no SyncCommand could ever be sent and RequestFullSync was unreachable although defined, handled and implemented; the full-sync framing wrote [length][bincode] while its only reader expected [compressed][length][bincode], shifting everything by a byte; and the document batch was bincode-encoded from Vec<serde_json::Value>, which serialises but can never deserialise, because reading a self-describing type means asking the format what comes next and bincode stores no type information to answer with. Full sync had therefore never once completed. The sender is kept, a joining node asks the seed for a full sync, and a failure to ask is logged as an error stating exactly what the node will and will not have.
  • A joined member knows the cluster it joined--advertise 10.0.0.1:6746 became 10.0.0.1:6746:6746, because the port was appended unconditionally to a value that already carried one. That address does not resolve, and the one place it mattered — the seed answering a JoinRequest with the peer list — discarded its error. So a joining node was a member from the seed's point of view and knew nobody from its own: solidb_cluster_healthy_nodes read 2 on the seed and 1 on the member, with nothing logged on either side. A port is now attached only when there is not one, bare IPv6 gets brackets first (appending to ::1 gives ::1:6746, which parses as a different address and fails only at connect time), and the send logs its failure.
  • Sync frame decoding is symmetricencode returned a frame and decode took a body, so every caller stripped a header whose length lived only inside encode. Thirteen tests had been sliced to the old 4-byte header and failed with the same error the header change was made to fix. There is now decode_frame and HEADER_LEN, all fourteen call sites go through them, a short frame and a frame that lies about its length are refused by name rather than by a discriminant error, and a test fails if the constant and the writer ever disagree again.
  • The native driver authenticates its whole pool — authentication is per-socket state, but auth() sent its handshake through the round-robin sender, so exactly one connection of the pool was authenticated and the rest stayed bare; the next command landed on a bare one and failed with Authentication required. Every pool_size > 1 client was unusable, including the Rust crate's own benchmark binary — which is why the TCP transport read as broken rather than merely unmeasured. With it fixed the benchmark completes for the first time: 21,606 sequential inserts/s against HTTP's 9,967 (2.2×) and 40,378 reads against 10,646 (3.8×).
  • The driver's query handler caches like /cursor does — it called parse directly where the HTTP handler uses the prepared-statement cache, and had no result cache at all, so it executed every query for real while HTTP replayed a memoized result. That measured as the binary protocol being 16% slower on a 50-row projection with nearly double the CPU per request (127 → 216µs) — “the driver is bad at queries” was really “one handler caches and the other does not”. Same cell after: 50,106 req/s against HTTP's 35,726 on 49–50µs of server CPU against 106–123µs — 1.40× the throughput on under half the CPU. Command::Query gains a cache flag (default true, so older clients keep the cached path) to opt out the way /cursor does with "cache": false.
  • Document writes over the driver invalidate the query cache — insert, update, delete and bulk over the native protocol left the shared query-result cache serving stale rows to both the driver and the HTTP /cursor path. Each mutation now invalidates the collection, matching the HTTP handlers.

v0.33.0

July 27, 2026
  • Columnar collections are a first-class SDBQL data source — a columnar collection was reachable from SDBQL through exactly one shape, FOR x IN c COLLECT AGGREGATE … RETURN …. Adding a FILTER, SORT, LIMIT or join made the same collection report CollectionNotFound, because columnar rows are not stored under the document prefix the scanner walks. FOR now resolves columnar collections directly, so filters, sorts, limits, joins and subqueries work — and columnar data can be combined with documents, edges and vector search in one query. Filter and projection pushdown are not implemented yet, so a selective filter over a large collection still reads less through the /columnar/…/query endpoint.
  • Columnar aggregate fixes — four bugs that produced wrong results rather than errors: the RETURN clause was ignored (RETURN {sum: total} came back as {"total": …}, and a scalar RETURN came back as an object); grouped queries dropped every aggregate after the first, so AGGREGATE lo = MIN(…), hi = MAX(…) lost hi; group and aggregate columns were reported under internal storage names instead of the COLLECT variables; and string group keys were double-encoded, so a came back as "\"a\"". The last of these was fixed in the storage layer, so the /columnar REST endpoint benefits too.
  • Index definitions replicate — an index created on one node existed only on that node: there was no CreateIndex operation in the sync protocol, so peers ran unindexed scans and never enforced a unique index they did not have. Worst, the TTL sweep skips a collection with no TTL index, so documents expired on the node where the index was created and lived forever on every other node. Index creation is also shard-aware now — previously an index on a sharded collection was built on the logical collection, which holds no documents.
  • Physical backupPOST /_api/backup (admin) takes a RocksDB checkpoint of the whole instance: near-instant, hard-linked, point-in-time consistent across collections. Restore by pointing a server at the directory. solidb-dump remains the per-database and cross-version path. Because a checkpoint hard-links SSTs on the same filesystem, copy it elsewhere — it is not protection against losing that volume.
  • Offline sync writes are persisted/_api/sync/push validated the session, appended to the replication log and returned {"accepted": N} without ever writing to storage, so pushed documents never existed. /_api/sync/conflicts and /_api/sync/resolve now return 501 instead of an empty list and {"success": true}; real conflict detection needs per-document version vectors, which storage does not yet carry.
  • Blob under-replication repair — blob chunks replicate inline at upload and are absent from both recovery paths, so a chunk missed while a peer was down stayed missing. The rebalance worker now scans for under-replicated chunks and re-pushes them.
  • Supply-chain checks in CIcargo-deny (advisories, bans, licenses, sources), clippy widened to --all-targets --all-features, and a declared MSRV verified by a job that builds it. Two advisories were fixed by version bumps rather than ignored.
  • Safer solidb-dump / solidb-restore — columnar indexes go through columnar_index records rather than schema indexed flags, document dumps are enveloped to avoid field collisions, index-list failures surface, and a partial dump or restore exits non-zero. Adds --scheme, --overwrite (import mode=upsert), path encoding and auth validation.
  • Dropping a database requires typing its name — the admin confirm modal takes a name confirmation, and the endpoint rejects the delete unless it matches.
  • Small responses are no longer gzip-ed — compression had a 32-byte floor, so every client sending Accept-Encoding paid a fixed per-response CPU cost on replies too small to benefit: a ~300B cursor response spent ~216µs compressing (65% of the request envelope) and a 65B health response grew to 88B. The 1-doc query path measured 42.7k req/s with compression against 103k req/s without. The floor is now 4 KB, overridable with SOLIDB_GZIP_MIN_BYTES if you are bandwidth-bound rather than CPU-bound.

v0.32.2

July 25, 2026
  • Blob collections are restorable from a whole-database dumpsolidb-dump streamed the server's single-collection /export output verbatim, and those records name neither the database nor the collection, so solidb-restore aborted on the first one with "No collection specified in doc or args". The dump now injects the routing metadata, copying binary payloads through byte for byte.
  • Collections larger than 10,000 documents are no longer silently truncated — the dump asked for batchSize 1,000,000 and read only the first response, but the server clamps it to 10,000. It now follows the cursor, and warns when the number of documents dumped differs from the reported count.
  • Empty collections survive a round trip — a collection with no documents and no indexes wrote nothing at all and disappeared from the dump. Every collection now leads with a declaration record carrying its type, so edge, blob and timeseries collections come back as themselves rather than plain document collections.
  • Columnar collections are dumped and restored — they live behind their own API and were skipped entirely, while their backing _columnar_* column family was exported as a phantom empty document collection.
  • Restore skips an unroutable record instead of aborting the whole run, creates the database once per run rather than once per collection, and treats "already exists" index clashes as success.

v0.32.1

July 24, 2026
  • solidb-restore honours --database / --collection — both are documented as overrides, but the target was resolved as "name embedded in the dump, falling back to the flag". Since every dump emits _database and _collection, the fallback was unreachable and both flags were silently ignored: solidb-restore -d staging --input prod.dump restored into prod. The flag now wins and the dump's name is the fallback, across document, blob-chunk and index records. Operators who relied on the old behaviour to restore a dump back into its original database can simply omit -d.

v0.32.0

July 19, 2026

Features

  • Windows x86_64 builds — releases now include solidb-windows-amd64.zip (solidb.exe, solidb-dump.exe, solidb-restore.exe) alongside the Linux and macOS tarballs. Three caveats for operators:
    • --daemon is Unix-only and exits with an error. Run SoliDB in a console, or wrap it with a service manager such as NSSM or sc.exe.
    • The generated .admin_password file is written with default ACLs rather than the owner-only permissions used on Unix. Restrict the data directory yourself on a shared machine.
    • FUSE (solidb-fuse) remains Unix-only, and solidb update still does not support Windows.

Changes

  • TLS moves from OpenSSL to rustls — OpenSSL is no longer in the dependency graph at all. The Docker image no longer installs libssl3, and building from source no longer needs libssl-dev (or Perl/NASM on Windows). Certificate validation now uses the platform trust store via rustls; ca-certificates is still required in the container image.

Performance

  • Vector-index persistence is throttled to at most once per 5s behind a dirty flag, with a shutdown flush, instead of re-serializing the whole index (all vectors + HNSW graph) after every write batch. Bulk loads into embedding-bearing collections are no longer O(batches × index size).
  • Document updates that leave every vector index's embedding unchanged skip the delete+reinsert entirely, so an incremental sync that only rewrites metadata pays no HNSW churn.

v0.31.0

July 14, 2026
  • Document versioning & time-travel — opt-in per-collection history; read the past with DOC_AS_OF / DOC_HISTORY. Enable it when creating a collection (new option in the admin modal and a versioning flag on the create-collection API) or toggle it later from the collection page. Covers single-document writes; history is capped by SOLIDB_MAX_VERSIONS (default 100).
  • Semantic query cache — opt-in (SEMANTIC_CACHE_ENABLED) in-memory cosine-nearest cache for generated-content queries.
  • Filtered vector searchVECTOR_SEARCH(coll, idx, vec, k, {filter, overfetch, ef}) over-fetches then post-filters, returning {doc, score} rows.
  • RERANK & RAG_PIPELINE — new SDBQL functions for lexical or LLM reranking and for composing GRAPH_RAG + rerank retrieval pipelines.
  • Scheduled materialized viewsCREATE MATERIALIZED VIEW … REFRESH "5m" recomputes a view on an interval.
  • Auto-embeddings for vector indexes — Create a vector index with embedding_source and SoliDB will automatically generate embeddings on insert using your configured LLM provider (OpenAI, Ollama, Gemini). Works in REST, driver, and SDBQL paths.
  • Improved stream processingCREATE STREAM windows now properly process events, support sliding/tumbling, and persist results into _streams:<name> collections that are fully queryable and live-queryable.
  • Graph analytics functions — New SDBQL functions PAGERANK(edge_collection, opts?) and DEGREE_CENTRALITY(edge_collection) for computing centrality directly in queries.

v0.26.4

June 11, 2026

Security

  • Per-database authorization on the data plane — every /_api/database/{db}/... route now enforces role permissions and API-key database scope. Set SOLIDB_DB_AUTHZ_MODE=warn for dry-run.
  • Cluster control messages are HMAC-signed — when a keyfile is configured, membership/heartbeat/rebalance messages are signed. All nodes must upgrade together.
  • Keyfile required for clusters and replication slowloris protection.
  • Lua resource limits — 64 MB memory cap and 30 s execution deadline.

Fixes

  • Truncate now replicates from every path.
  • Stream processors clear buffered window on truncate.
  • Materialized-view refresh reports removed document counts.

Performance

  • JOINs scan the joined collection once.
  • Graph traversals and shortest-path queries optimized (single edge scan).
  • Role lookups and key cache improvements.

Previous Releases

v0.21.2 (2026-03-24)
  • SORT performance & correctness improvements (stable sort, index optimization, pre-evaluation).
v0.9.0 / v0.8.0 (2026-02)
  • ACID transactions on node.
  • Columnar storage and indexes.
  • Vector quantization, HNSW, JOIN support, schema validation, many SDBQL enhancements.
  • Full list of features and fixes in CHANGELOG.md.

For the complete detailed history including all minor commits, see the CHANGELOG.md file in the repository.