Changelog
Release notes for the RushDB engine and SDKs, newest first.
Changelog
This project follows Semantic Versioning. Release notes
describe observable engine and protocol changes; migration instructions belong
in docs/manual/operations/upgrading-from-2x.md.
3.0.3 — 2026-11-05
No engine, protocol or storage changes: the binary behaves exactly like 3.0.2.
Changed
- The Bolt and Cypher manual pages enable Cypher with
--languages sql,cypher.--languagesreplaces the default set, so--languages cypheralone turned SQL off, and RushDB Cloud's Smart Search needs SQL.
3.0.2 — 2026-11-02
No engine, protocol or storage changes: the binary behaves exactly like 3.0.1.
Changed
- Package and image metadata use RushDB's current description, "the database
for AI": the container image label, the
rushdb-servercrate description and keywords, the Homebrew formula (homepage now https://rushdb.com) and the Helm chart description. - The Apple silicon binary is built on macOS 15.
3.0.1 — 2026-11-01
Changed
- Vector indexes have their own memory budget,
resources.vector-cache-bytes(default 256 MiB), instead of sharinglogical-cache-byteswith text and planner caches. A space of 1M 768-dimension vectors now answers from its index on default settings (it fell back to exact search before).
Fixed
-
Data-access policy (security): under an enforced policy, a record query that filters, sorts, groups or aggregates on a hidden property, or traverses a denied relationship type, is refused with
unauthorized. Before, the output was masked but the hidden values still decided which records came back and in what order. A traversal must name its$relationtype when the policy denies any type, and its endpoint label unless every label is allowed. -
Nested logic:
$or/$and/$nor/$xor/$notinside a label block that is itself inside a block ({ DIAGNOSIS: { CONCEPT: { $or: [...] } } }) silently matched nothing, and with a$relationbeside it the query failed. The inner block was read as a property; it is now parsed as the root parses it. -
retypeRelations: merging into a pair that already has the target type keeps every relationship property from both sides (the existing relationship's values win a conflict, and it keeps its id). Before, the retyped relationships' properties were dropped, and a newly created relationship carried none.affectednow counts relationships retyped, and the response reportsmergedandpropertyConflicts. A newmergeStrategyoption picks the winner of a conflict:keep(default, as above),append(the retyped value wins),rewrite(the retyped relationships' properties replace the existing one's) orfail(refuse withconflict, writing nothing, and list the first 20 conflicts). -
Building an indexed filter's match set no longer re-counts the set for every owner it adds: the cost was quadratic in the tenant's size. The set is also kept per snapshot and shared by its readers, so repeated pages of an indexed filter no longer re-read the index.
-
A vector search never waits for an index build: past a small tail of unindexed vectors it answers exactly (
fallback: "annWarming") while the index is built in the background. -
A relationship created in the same statement or batch as its new endpoint records is counted in the relationship catalog:
describe.schemashows it and SQL has itsrushdb_edgestable. Relationship headers now apply after record headers in every commit. -
Cypher, GQL and Gremlin writes that create relationships stage in transactions, and so over Bolt in explicit and driver-managed transactions; they failed with "transaction stage does not support operation … yet".
-
Bolt reports the query type (
w,rw,r) andstats: {"contains-updates": true}in the summary that ends a write's result; drivers sawquery_type = 'r'and no updates. -
A Cypher
CREATEproperty computed from an expression (r[0],CASE, arithmetic, a function call) fails withunsupportedand theWITHworkaround instead of an internal execution error (a Grafeo limitation). -
SQL/PGQ: two anonymous nodes in one pattern are separate nodes; they were joined as one (a Grafeo defect), which also made
a <> bmatch nothing. -
Mongo-style
findapplies its inclusionprojection: documents keep only the projected fields and_id("_id": 0drops it). -
Describing a label's properties without a filter reads only its sample records, and value search skips columns whose type cannot hold the text; property ranges come from the value dictionary's first and last entries. None of this changes a result.
-
Value search over long texts reads only the records that hold the column and matches ASCII text in place.
-
A name held under several types on one label is searched once:
totalno longer counts its matches twice. -
$typematches the type each record holds when a name is stored under more than one type ($type: "number"no longer matches text values). -
Value counts, top values and value lists over arrays count each element once per record, as distinct counts already did.
-
A
selectof metrics withoutgroupByover no matches returns one row (counts 0, other metricsnull), as SQL does. -
An unfiltered property description samples every label, so a label written after 10,000 others' records still shows sample values.
-
An in-memory node no longer rejects writes when the host disk is below the free-space threshold: the guard protects a data directory.
-
The PostgreSQL wire
INSERTtag isINSERT 0 n: psql no longer reports "could not interpret result", and psycopg'srowcountis the count, not -1. -
The PostgreSQL wire accepts the client parameters drivers and tools set on every connection (
application_name,extra_float_digits,DateStyle,TimeZone,client_encoding,search_path, …) andSHOWanswers them; Metabase sentSET application_nameand could not connect.SETof another parameter fails with0A000naming the accepted ones. -
current_database(),current_userand the other session functions work beside other expressions over the PostgreSQL wire, not only alone. -
SQL
UNION/UNION ALLworks; it panicked inside DataFusion and returnedunavailable(503). A panic inside the SQL engine now fails only its statement, withexecution_failed. -
SQL statement errors carry their class instead of "internal execution error": an unknown column or function is
not_found(42703/42883over pgwire), a mixed-type column compared with a number isinvalid_request(22P02) namingTRY_CAST, an unimplemented type or feature isunsupported. -
SQL
SET,PREPARE,LISTEN, cursors,COPYand table DDL fail withunsupported(0A000) and the reason, not "send writes as mutate.statement". -
SQL
CREATE INDEX … ON supplier (region)indexes the stored label (SUPPLIER) aSELECTonsupplierreads; it declared an index on a labelsupplierthat nothing used.
3.0.0 — 2026-10-28
First standalone release of the Rust engine.
Installation
- One-line installer for Linux (x86-64, arm64; glibc 2.28+) and macOS
(Apple silicon, Intel; macOS 11+). It verifies the release checksum, and
the Sigstore signature when
cosignis installed. - Container image
rushdb/rushdb(alsoghcr.io/rush-db/rushdb) forlinux/amd64andlinux/arm64, signed, with an SBOM and build provenance. The repository'sDockerfilealso builds it from source. - Homebrew formula in
rush-db/tap, with abrew servicesdefinition. - Helm chart
oci://ghcr.io/rush-db/charts/rushdb: a single-node StatefulSet with a volume claim, a Service and an optional Ingress. - Deployment templates in
deploy/: Docker Compose (plain, and behind Caddy with automatic TLS), cloud-init for Linux VMs, the systemd unit, Fly.io, Render (with a Deploy button), Coolify and Dokploy. - The README is translated into Chinese, Japanese, Korean, German, French, Portuguese, Spanish, Hindi, Arabic, Indonesian and Thai.
cargo binstall rushdb-serverinstalls the release binary;cargo install rushdb-servercompiles it.- Linux release binaries are built against glibc 2.28, and Linux arm64 is a new target.
CLI
- One binary,
rushdb:rushdb serveruns the server (it was the separaterushdbdbinary), andrushdb upruns it in the same process with a./.rushdbdata directory, takes every server option, and prints where the node is listening. The container image isrushdb/rushdb. - New
rushdb healthcommand: exits 0 when the node's/healthanswers.
Queries
- A filter without a usable index runs as a scan of the records it names,
as in PostgreSQL and Neo4j, instead of being refused over 1,000,000
records.
unindexed-walk-max-ownersnow defaults to0(no limit). - A request can bound its own queries, never loosen the server's bounds:
context.deadlineMs,context.queryMemoryMaxBytesandcontext.unindexedWalkMaxOwnersin the protocol envelope, and over the PostgreSQL wire the session settingsstatement_timeout,work_mem,disallow_full_table_scansandlarge_full_scan_rows(SET,SHOW,RESET).context.deadlineMsover HTTP shortens the server's timeout; it was ignored. - SQL reads stream label tables and plan their filters on declared indexes,
so
SELECTworks on labels with millions of records;count(*)without a filter reads the label's record counter. A SQL scan stops at the request's deadline between batches. - An upsert on a
mergeBykey without an index counts the key values it holds againstquery-memory-max. - SQL
INSERT,UPDATEandDELETEread their rows likeSELECTand commit only the records they touch, so they work on labels of any size; the planned writes count against the request budget.rushdb_edgestables stream too, so the 100,000-relationship limit is gone, and resolve their columns and row count from the relationship catalog without reading relationships. - Unknown tables in SQL errors suggest close names (
Did you mean 'Patient'?), for reads and forINSERT/UPDATE/DELETEtargets.
Catalog
propertyValuestakes asearch(contains,prefix,exactorsimilar), answered from the value dictionaries; a date such as1993/11/12or2013-06also matches that date stored in another layout, anddomainOnMissanswers a search with no match by listing a small property's values.query.searchValuessearches every property at once.describe.namessuggests labels and properties spelled like a name;describe.schemaSummaryreturns every label with its properties, distinct counts, ranges and few values in one read.describe.valueOverlapreports, for property pairs, how much of one's values the other holds, over the exact value dictionaries: the evidence for join suggestions.- Under an enforced data-access policy the catalog reads and SQL
SELECTsee only the allowed labels, their visible properties and the allowed relationship types, instead of failing withunauthorized. A policy'sminCountwithholds catalog groups counted over fewer records. - A policy's
allowedLabels: ["*"]allows every label, including labels created later, so a policy can hide properties or setminCountwithout listing labels. An emptyallowedLabelsallows none (lockdown). Writes compare allowed labels case-insensitively, as reads do, and a write to a label outside the list fails withunauthorizedinstead of an internal error. - The value dictionaries these reads hold count as query working set
(
query-memory-max, or the request'scontext.queryMemoryMaxBytes) and stop at cancellation and the request deadline. Past the cap,describe.valueOverlapanddescribe.schemaSummaryfail withresourceExhausted;query.searchValueslists the property inskipped. propertyValueswithout a predicate answersvaluesand non-numericrangefrom the value dictionaries; before, both read every record of the label (2 s for a 3-value column of a 1M-record label). Under one label and one stored type, distinct counts, numeric ranges (describeandpropertyValues),topKand limitedvaluesno longer hold every distinct value: a unique-id column of more than ~3M records failed withresourceExhaustedat the defaultquery-memory-max.propertyValuescountandtopKover a scanned property (long texts, arrays, filtered queries) hold a digest per distinct value instead of the value, so a column of long texts is counted within the working set.describe.schemaSummarycounts a text property whose values are too long for the value dictionary, so every scalar property reportsdistinctCountand fewvalues; before, such a property had neither whilepropertyValuesreported them. The count comes from write-time refcounts of the long values' digests (storage format 6), so it costs no scan at any size; listing them reads at most 65,536 records, past which the values are left out and the count stays. A data directory written by an earlier build (format 5) does not open and must be reloaded. Type-split entries name theirsiblingTypes, asdescribe.propertiesdoes.- Catalog introspection (
describeschema and properties,schemaSummary) cuts every text sample, listed value and bound toresources.introspection-value-max-charscharacters (default 64; 0 = no cap), ending with…, so long texts cannot flood a schema read. Value queries and records keep values whole.
Fixes
- Numbers in request JSON are parsed exactly: some floats were stored one
unit in the last place off (
18.916268423199654as18.916268423199657). - An unfiltered
describe.propertiesreports the exactmin/maxover every record; it reported the range of the first 10,000 records sampled. siblingTypeslists the types a property holds on the described label; it listed the types the name held anywhere in the tenant.- A property stored as text on one label no longer hides another label's
numeric range in
describe.schemaSummaryandpropertyValuesrange. describe.namesranks the name spelled exactly first among equal scores, then a label before a property.- A number property whose first value is a float filters and describes its
later integers correctly:
{"x": {"$gte": 1}}skipped them and the catalog read them as their bit patterns (5e-324). An integer after a float is stored under its own integer definition, as a float after an integer already was; inside an array, integers mixed with floats are stored as floats. - Explicit nulls are counted once, as the property's
nullentry, beside one entry per value type that counts only values of that type. Nulls written before a property's first value were not counted, and later nulls were counted under whichever type existed.distinctCountno longer countsnullas a value. - A property stored as two types on one label: each
describe.schemaSummaryentry reports its own distinct count, values and range, andpropertyValuesrangecovers one ordered class (numbers, else datetimes by instant, else text) instead of mixing text with datetimes. - Signed ISO years (
-0032-01-01T08:00:00Z) are datetimes, ordered before year0000and rendered signed; with a time they were stored as text, so a property of BC and AD dates split by sign. describerejects unknown fields asinvalid_request(aschemaSummarysent aqueryignored it), anddescribe.schemawithdetail: "catalog"and a predicate isinvalid_requestinstead of an internal error.rushdb servewithout--languagesenables the documented default set (sql,gremlin,sql-pgq,mongodb) and the PostgreSQL wire listener. It previously enabled no statement language.- The
rushdb listeningstartup line is logged (it was filtered out) and includes the listen address. - A startup error such as the default-token refusal exits with a message instead of a panic.
- SQL results keep the statement's column order over PostgreSQL wire and HTTP; columns were sorted by name.
- Bolt encodes floats and booleans with the PackStream markers drivers
expect. The official Neo4j drivers misread every boolean and float before,
including
has_more, so a result failed after its first page. - Bolt answers messages after a failure with
IGNOREDuntilRESET, so a driver reports the failure that happened;RESETalso ends an open transaction instead of leaving later statements staged in it. - A read inside a Bolt transaction reads committed data instead of returning
no rows, so the drivers' managed read transactions and
execute_querywork. BEGIN,COMMIT,ROLLBACKand the other transaction commands fail over the PostgreSQL wire with SQLSTATE0A000and a message naming autocommit, instead of a parse error.
2.x
The final Neo4j-backed application architecture is maintained at the 2.x branch
of rush-db/rushdb. See the 3.0 migration guide for supported boundaries.