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, 2026solidb-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 overGET /_api/databases. That endpoint filters by permission, so this captures exactly the databases the principal may read —_systemincluded, with its credential collections skipped as everywhere else. Conflicts with-dand-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-cis 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_systembookkeeping into a live server.- Fixed: the driver protocol's
list_collectionsstill 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_envdiscloses that a database holds provider keys. - Fixed: a database with an
_envcollection could not list its collections —GET /_api/database/{db}/collectionenumerates column families, so_envappeared 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 a403for 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._envis 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}/envendpoints 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, standaloneOFFSET— combine query blocks withUNION [ALL],INTERSECTandEXCEPT(either side may be parenthesized; duplicates removed except forUNION ALL; chains follow SQL precedence, soINTERSECTbinds tighter thanUNION/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 exprdeduplicates rows.COLLECT … INTO g KEEP v1, v2restricts which variables land in the group arrays.NONE(x IN arr SATISFIES cond)andNONE(arr, x -> cond)are true when nothing satisfies.OFFSET nworks alone or asLIMIT n OFFSET m. - Native HTTPS termination — new
--tls-cert/--tls-keyflags 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=1refuses plaintext there. Both flags are required together. - Per-client API rate limiting — only
/auth/loginwas throttled before; now the whole router has a per-client-IP sliding-window limiter (default 600 requests / 60s) answering429withRetry-Afterbefore any handler work. Internal cluster traffic and CORS preflights are exempt. Configure withSOLIDB_API_RATE_LIMIT(0 disables) andSOLIDB_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
QueryandExplainnow 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/wsand/_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.1unless--hostorSOLIDB_HOSTsays otherwise. Use0.0.0.0only when something in front of the process terminates TLS. - Query-driven auto-indexes (opt-in) — collection
autoIndex: true, or unset plusSOLIDB_AUTO_INDEX=1. AFOR+FILTERmiss 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. Explicitfalseoverrides the env var. SORT, null filters, sharded collections, collections aboveSOLIDB_AUTO_INDEX_MAX_DOCS(default 1,000,000), and fields no document carries are not auto-indexed.EXPLAINreports 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_ROLESand 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 index —
create_indexwrote 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=1skips 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/_servicesrequired only Write, so a collection editor could publish unauthenticated/api/{db}/{service}/…handlers. Those mutations now need Admin. New services default torequire_auth: true.solidb.envsecrets are injected only for authenticated admin scripts. - Cluster control-plane HTTP is Admin —
remove-node,rebalance, sync-log prune/stats, cluster info/status, blob rebalance, and the cluster status WebSocket accepted any authenticated principal, includingviewer. They now require Admin. - Livequery JWTs are path-restricted on
?token=as well as Bearer. JWT roles for real_adminsusers are reloaded on each request. API keys must declare at least one role (empty no longer defaults toadmin). /metricsrequires authentication unlessSOLIDB_METRICS_PUBLIC=1. PresentSOLIDB_METRICS_TOKENor an admin JWT.- Physical backups are jailed under
SOLIDB_BACKUP_ROOT(or{data_dir}/backups). Webhook URLs are SSRF-checked.SOLIDB_DB_AUTHZ_MODE=warnandSOLIDB_LUA_FAST_MODEare ignored without their explicit unsafe companion flags. - Passwords must be at least 12 characters. The admin app no longer falls back to
admin/admin. Luacrypto.jwt_decodecompares signatures in constant time. - SDBQL string functions are AQL-shaped and Unicode-correct — offsets and
LENGTHon strings are Unicode scalar counts (BYTE_LENGTHis UTF-8 bytes). AddedLIKE()as a function,REGEX_MATCHES,REGEX_SPLIT,REPEAT,LPAD/RPAD,JOIN,MASK,WORD_COUNT,TRUNCATE_TEXT,RANDOM_TOKEN.ENCODE_URIpercent-encodes UTF-8. Null string arguments propagate as null. Regexes are compiled throughsafe_regexand cached.REPEAT/ pad results are capped at 1 MiB. - SDBQL array, math, and object functions the reference already listed now exist —
TAKE,DROP,CHUNK,ZIP,CONTAINSon arrays;MOD,CLAMP, variadicMIN/MAX;GET,DEEP_MERGE,ENTRIES,FROM_ENTRIES,JSON_POINTER.NTHaccepts a negative index.VAR_SAMP/STDDEV_SAMPdivide byn-1.RANGErefuses more than 1M elements.SHIFT([])no longer panics. - SDBQL function dispatch is prefix-routed —
UPPERno longer walks theDATE_*/SQRTmatch arms. AddedBIT_AND/BIT_OR/BIT_XOR/BIT_NEGATE/BIT_SHIFT_*,OUTERSECTION,IS_DATE,IS_KEY,GEO_EQUALS.COUNTon an object is the key count.TO_NUMBER(null)is null. - SDBQL date functions share one parser —
YYYY-MM-DDand second-resolution epochs parse.DATE_ADDuses calendar months (31 Jan + 1 month → 28/29 Feb). AddedDATE_COMPARE,DATE_LEAPYEAR,DATE_MILLISECOND,DATE_ISOWEEKYEAR.DATE_DIFFunit is optional.DATE_TRUNCacceptsweek. Null date arguments propagate. - SDBQL set and slice helpers no longer walk the array twice —
UNIQUE,UNION,INTERSECTION,MINUSandCOUNT_DISTINCThash values (collision-checked) instead ofserde_json::to_stringorVec::contains.APPEND/UNSHIFT/FLATTENreserve;SHIFTcopies the tail instead ofremove(0).SORTEDusessort_unstable_by.KEEP/UNSETcopy only surviving keys. ASCIISUBSTRINGslices 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_INFOcatalog;GRAPH nameresolves the first edge collection;MATCH (a:coll {_key: k})-[:e*1..n]->(b).CREATE_VIEWsearch aliases;SEARCHscored filter;SEARCH_INDEXfulltext;TOKENS/PHRASE/BOOST. GeoJSON constructors plusGEO_CONTAINS/GEO_INTERSECTS/GEO_IN_RANGE/GEO_AREA.PARSE_IDENTIFIER, recursive keep/unset,ZIPobject form,ZIP_OBJECT,DATE_ROUND,APPLY/CALL,MINHASH.CAN(action, doc)uses_acl/owner. Batch and transactional writes are versioned.VALID_TIME AS OFfiltersvalid_from/valid_to. - SDBQL time-series, sketches, semantic operators, and HOFs —
MAP/FILTER/FLAT_MAP/GROUP_BY/SORT_BY/WINDOW_BYtake lambdas without|>.<=>is vector cosine distance or a three-way compare; binary~is trigram match. AddedDELTA,RATE,FILL,RESAMPLE,ASOF JOIN, HyperLogLog / percentile / top-k sketches,MATCH_SEQ,REDACT. GraphFOR v, e, pplusPRUNE.SYSTEM_TIME AS OFandSNAPSHOT_DIFFon 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,LENGTHreturned the document count instead of the character length. UseCOLLECTION_COUNT("users")orLENGTH((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/cronroutes); eight driver-protocol commands (list_queues,list_jobs,enqueue_job,cancel_job, and the four cron commands); the Lua globaldb:enqueue(queue, script, params, options); and the Rust itemsqueue::{CronJob, QueueConfig}andQueueWorker::check_cron_jobs. Per-queue pause and concurrency settings go with them — they were reachable only through the removed endpoint, so_queue_configcan 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
5is 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 theJob/JobStatustypes 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_WORKERSnow sizes that maintenance worker rather than an application queue. - Action required — if you called any endpoint, driver command, SDK sub-client or
db:enqueueabove, move that work to Soli's job engine. Let the queue drain before upgrading: apendingrow that no trigger created will never run. Stored_cron_jobsand_queue_configcollections 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_threaddefaults 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_sizeunset, total memtable RAM is the number of write-active collections times 64 MB with nothing forcing an early flush; withmax_open_filesat-1no SST is ever closed; and withcache_index_and_filter_blocksoff, 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 aSOLIDB_*environment variable, applied on top of whichever preset is in force;--devstays 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_LIMITdefaults to0. 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 a429instead 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 withoutSOLIDB_TRUST_PROXY_HEADERS=1— so each can sit well inside the budget while the total goes past it and all of them start seeing429s. 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 — soSOLIDB_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—_envis where SoliDB tells you to put provider API keys, but it was an ordinary collection: any principal withReadon the database could dump every key throughGET /_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/envendpoints requireAdminon the database — which is what the documentation already promised. Server-side readers (the LLM client, authentication, the Luasolidb.envbinding) are unaffected. Because credentials are no longer reachable through the query API,solidb-dumpskips 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--advertiseflag, 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 answered401to 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 setSOLIDB_ADMIN_PASSWORD. - A joining node can ask for the data that predates it —
_adminsnever reached a node that joined, so the peer answered401forever 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 noSyncCommandcould ever be sent andRequestFullSyncwas 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 wasbincode-encoded fromVec<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:6746became10.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 aJoinRequestwith 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_nodesread 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::1gives::1:6746, which parses as a different address and fails only at connect time), and the send logs its failure. - Sync frame decoding is symmetric —
encodereturned a frame anddecodetook a body, so every caller stripped a header whose length lived only insideencode. 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 nowdecode_frameandHEADER_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. Everypool_size > 1client 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
/cursordoes — it calledparsedirectly 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::Querygains acacheflag (default true, so older clients keep the cached path) to opt out the way/cursordoes 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
/cursorpath. 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 aFILTER,SORT,LIMITor join made the same collection report CollectionNotFound, because columnar rows are not stored under the document prefix the scanner walks.FORnow 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/…/queryendpoint. - Columnar aggregate fixes — four bugs that produced wrong results rather than errors: the
RETURNclause was ignored (RETURN {sum: total}came back as{"total": …}, and a scalarRETURNcame back as an object); grouped queries dropped every aggregate after the first, soAGGREGATE lo = MIN(…), hi = MAX(…)losthi; group and aggregate columns were reported under internal storage names instead of theCOLLECTvariables; and string group keys were double-encoded, soacame back as"\"a\"". The last of these was fixed in the storage layer, so the/columnarREST endpoint benefits too. - Index definitions replicate — an index created on one node existed only on that node: there was no
CreateIndexoperation 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 backup —
POST /_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-dumpremains 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/pushvalidated the session, appended to the replication log and returned{"accepted": N}without ever writing to storage, so pushed documents never existed./_api/sync/conflictsand/_api/sync/resolvenow return501instead 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 CI —
cargo-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 throughcolumnar_indexrecords rather than schemaindexedflags, document dumps are enveloped to avoid field collisions, index-list failures surface, and a partial dump or restore exits non-zero. Adds--scheme,--overwrite(importmode=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-Encodingpaid 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 withSOLIDB_GZIP_MIN_BYTESif you are bandwidth-bound rather than CPU-bound.
v0.32.2
July 25, 2026- Blob collections are restorable from a whole-database dump —
solidb-dumpstreamed the server's single-collection/exportoutput verbatim, and those records name neither the database nor the collection, sosolidb-restoreaborted 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
batchSize1,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, 2026solidb-restorehonours--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_databaseand_collection, the fallback was unreachable and both flags were silently ignored:solidb-restore -d staging --input prod.dumprestored intoprod. 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, 2026Features
- 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:--daemonis Unix-only and exits with an error. Run SoliDB in a console, or wrap it with a service manager such as NSSM orsc.exe.- The generated
.admin_passwordfile 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, andsolidb updatestill 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 needslibssl-dev(or Perl/NASM on Windows). Certificate validation now uses the platform trust store via rustls;ca-certificatesis 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 aversioningflag on the create-collection API) or toggle it later from the collection page. Covers single-document writes; history is capped bySOLIDB_MAX_VERSIONS(default 100). - Semantic query cache — opt-in (
SEMANTIC_CACHE_ENABLED) in-memory cosine-nearest cache for generated-content queries. - Filtered vector search —
VECTOR_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 views —
CREATE MATERIALIZED VIEW … REFRESH "5m"recomputes a view on an interval. - Auto-embeddings for vector indexes — Create a vector index with
embedding_sourceand SoliDB will automatically generate embeddings on insert using your configured LLM provider (OpenAI, Ollama, Gemini). Works in REST, driver, and SDBQL paths. - Improved stream processing —
CREATE STREAMwindows 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?)andDEGREE_CENTRALITY(edge_collection)for computing centrality directly in queries.
v0.26.4
June 11, 2026Security
- Per-database authorization on the data plane — every
/_api/database/{db}/...route now enforces role permissions and API-key database scope. SetSOLIDB_DB_AUTHZ_MODE=warnfor 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
- SORT performance & correctness improvements (stable sort, index optimization, pre-evaluation).
- 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.