ArcadeDB 26.9.1 is the largest release ArcadeDB has ever shipped: 992 issues and pull requests closed under the 26.9.1 milestone, 675 issues and 317 PRs, out of 655 pull requests merged and 1,500 commits since 26.8.1.
At a Glance
| Area | What changed | The number |
|---|---|---|
| Backup and restore | Parallel compression, parallel restore, no flush suspension | 27.6x faster backup, 77% writer throughput during it |
| Storage integrity | Records larger than a page: lost updates, phantom rows, leaked chunks | 16% of chunk slots were orphaned and never reclaimed |
| Query correctness | NOT IN, DISTINCT ... LIMIT, multi-key GROUP BY, in-transaction range scans |
5 independent wrong-result defects |
| Index usage | IN (...), BETWEEN, composite prefix + ORDER BY, @rid IN [...] |
1143x on @rid IN [...] at 400k documents |
| Vector search | Constant-time open, scheduled rebuilds, adaptive efSearch |
recall@10 back to 0.92 past 10,000 vectors |
| Security | 6 advisories, pre-auth DoS on three wire protocols, regex backtracking | all affecting 26.8.1 and earlier |
Upgrading is strongly recommended for every deployment. There are breaking changes and behaviour changes; no schema migration is required, and no existing database is rewritten.
Major Highlights
Backups Are 27.6x Faster and No Longer Stall the Database
A full backup ran single-threaded deflate at level 9, CPU-bound at 20-40 MB/s, and it suspended page flushing for the whole window: dirty pages piled up until arcadedb.flushSuspendMaxDeferredRAM was reached, committers were throttled, and LSM compaction was postponed. HA snapshot shipping and cluster verify did the same thing (#6072, #6075, #6086).
Measured on a 1.25 GB database:
| Measurement | Before | After |
|---|---|---|
| Full backup | 18.88 s | 0.68 s (27.6x) |
| Concurrent writer throughput during the backup | 4.3% of baseline | 77% of baseline |
| Restore | 2.9 s | 0.68 s |
| Archive size | 323 MB | 348 MB (+7.5%) |
- Parallel compression, tunable with
arcadedb.backup.compressionLevel(new default 1, was 9),arcadedb.backup.compressionThreadsandarcadedb.backup.maxMBPerSecond. The ZIP format is unchanged and archives written by older versions restore normally. - Parallel restore, largest entry first, behind a 256 KB buffered read instead of
ZipInputStream’s unbuffered 512-byte reads (arcadedb.restore.threads). Even the sequential path went from 5.16 s to 3.96 s. - A page-level copy-on-write snapshot replaces flush suspension:
arcadedb.pageSnapshotEnabled,arcadedb.pageSnapshotMaxRAM,arcadedb.pageSnapshotMaxSize,arcadedb.pageSnapshotSpillPath. Backups, HA snapshot shipping and the/checksumsendpoint take a point-in-time view without ever stopping the flusher. - Two JVM-wide stalls are gone with it. The deferred-flush backpressure gate was process-wide, so one database’s backlog stopped the flush thread for every database on the server (#6200), and
publishPagesblocked inside the global page-manager lock whenever the flush queue filled, serialising the commits of every database behind one database’s write burst (#6259). Both are per-database now.
The trade is 7.5% archive size for 27.6x backup time. Set
arcadedb.backup.compressionLevel=9if the archive size matters more to you than the backup window.
Records Larger Than a Page: Lost Updates, Phantom Rows and a 16% Space Leak
A record that outgrows its page is stored as a chunk chain or behind a placeholder pointer. Re-triaging #5279 turned up a whole family of defects on that path, every one of them silent:
- A lost update. Two transactions updating the same placeholder-backed record (pointer on one page, content on another) both committed and one write vanished, with no
ConcurrentModificationException(#6141). The content page is version-checked now, so the conflict is raised. - A record returned twice. A
SELECTreturned a placeholder-backed record under two different RIDs when its content had spilled into chunks, socount(@rid)reported 2 for one record (#6196). - A 16% space leak.
CRUDTest.multiUpdatesOverlapended with 243,821 orphaned chunk slots out of 1,545,495, because a shrink ending exactly on a chunk boundary never freed the tail and nothing ever reclaimed them, although three code comments promised otherwise (#6319, #6294).CHECK DATABASE FIXnow sweeps them and reportsorphanedChunks/orphanedChunksReclaimed. - False conflicts. Reading a multi-page record failed with “was modified during read after N retries” when an unrelated record’s chunk on a shared page was written (#6217), and eight threads rewriting different large records on one page got
ConcurrentModificationException, five of eight exhaustingTX_RETRIES(#6129). Chunked head slots take part in the disjoint-slot merge now, and a read validates only its own chain. - A permanent size ratchet. A chunked record’s head chunk shrank to the smallest size it ever had and never recovered, so a record oscillating in size degraded for ever into a longer chain with unusable gaps (#6163). A record that shrinks back inside its slot is collapsed to a plain record again (#6178, #6286).
- A checker that reported a clean database. After
CHECK DATABASE FIXforce-deleted a record with a broken chain, the placeholder pointing at it was left dangling, socount(*)said 8 and a scan said 7, permanently, while the report saidtotalErrors: 0(#6292).
Free-space accounting was fixed with them (#6154, #6339), and a self-referencing edge-list chunk no longer hangs an ordinary traversal in a request thread (#6278).
Wrong Results in Ordinary SQL
Five independent defects, all of them returning a plausible answer to the wrong question:
NOT INreturned theINresult set.WHERE prop NOT IN [...]on an indexed property returned 1 row where 25,089 matched, because the index planner served the lookup without consulting the NOT flag. Present since 26.7.1 (#6796).SELECT DISTINCT ... ORDER BY ... LIMIT nreturned fewer thannrows, because the Top-K bound was applied before deduplication (#6923).- A multi-key
GROUP BYgrouped on the last key only: the synthetic alias counter wasfinal int i = 0outside the loop, so every key got the same alias (#6924).DISTINCTwas silently dropped whenever the statement also hadGROUP BY,UNWINDorexpand()(#6925). - An index range scan ignored its own transaction’s deletes. A range query inside a transaction returned rows deleted or re-keyed in that same transaction, including rows that did not match the
WHEREclause (#6927). - An indexed range over non-ASCII text returned nothing.
BinaryComparatorordered STRINGs by UTF-16 code units while LSM index pages order them by unsigned UTF-8 bytes, so a scan whose bounds fell where the two orders disagree returned zero rows although both keys were in range (#6997).
Also: the ?? null-coalescing operator always returned its right operand because the AST builder had no visitor for it (#6393); WHERE @rid > :param returned nothing while the same RID as a literal worked, breaking RID-cursor paging (#6188); @rid IN (SELECT ...) never matched (#7054); TRUNCATE TYPE inside an explicit transaction committed the caller’s transaction from the inside, so BEGIN; TRUNCATE TYPE; ROLLBACK destroyed 1,000 records (#6220); and EXPLAIN UPDATE ... submitted as sqlscript executed the update, which ran unbounded for hours in production (#6648).
Indexes Are Used Where They Were Not
| Query shape | Before | After |
|---|---|---|
WHERE prop IN (v1, ..., vN) (parenthesised literal list) |
full scan, ~350-400 rows/s, ~40 s per 15k batch | index lookup (#6640) |
WHERE k1 = ? AND k2 = ? ORDER BY ts DESC LIMIT 1 on a composite index |
full scan | composite prefix seek plus a directional range scan (#6592) |
WHERE n BETWEEN 15 AND 25 |
full scan (while n > 15 AND n < 25 used the index) |
index range (#5966) |
WHERE @rid IN [...] on a type |
full type scan, 82.12 ms at 400k docs | direct RID fetch, 0.07 ms (1143x) (#5824) |
Cypher MATCH (n:A\|B {id:'a1'}) |
scan of 1,000 records | per-root index seeks (#6397) |
Cypher MATCH (e:Child) WHERE e.id IN $ids, index on the parent type |
label scan | inherited NodeIndexSeek (#7021) |
WHERE LOWER(x) BETWEEN ... / LOWER(x) IN [...] on a COLLATE CI index |
full scan | index range (#6033, #6037) |
Two more index defects worth naming: INSERT followed by CREATE INDEX in the same transaction produced an index that was readable, reported healthy by CHECK DATABASE, and missing the record (#6324); and a composite index mixing a scalar property with one BY ITEM/BY KEY/BY VALUE property was never updated when only the scalar changed (#6934).
Vector Search: Opening a Database, Rebuilding the Graph, and Recall
Most of this was measured and reported by @tae898 on real corpora.
- Opening a database is constant-time again. Every open parsed every page of every
LSM_VECTORindex to rebuild the in-memory location map, about 1.4 s at 10M vectors, even for a session that never searched. The map is materialised on first use now (#6722). A Graph Analytical View was rebuilt by a full graph scan on every open too, 4.03 s at 1M vertices for an open-and-close with no query; the CSR is persisted at clean close with a freshness certificate and restored lazily (#6583, #6632). - Rebuilds stopped ambushing the first query. A session that inserted before its first search paid a full synchronous rebuild on the search thread: 2,618 ms versus 215 ms on 20,000 vectors, 128,543 ms at 1M (#6772). A persisted graph is reused as a prefix and only the gap is built (#6655, #6798). A single insert into a settled 50,000-vector index no longer triggers an 8-14 s rebuild 15 s later (#6496), and the rebuild threshold scales past 250,000 vectors where the linear delta scan was ~79% of query time (#6797).
- Recall stopped falling off a cliff. The adaptive
efSearchbeam narrowed from 100 to 20 once an index passed 10,000 nodes: recall@10 fell from 0.9200 at 9,000 vectors to 0.5420 at 11,000, for under 1 ms saved. The beam widens with graph size now, and an explicitefSearch: 100is honored (#6494). - A selective filter makes search faster, not slower. A RID allow-list made search slower the narrower it was (p50 2.020 ms unfiltered, 30.145 ms for 5 RIDs, against 0.090 ms for a direct pre-filter). A pre-filter plan scores the allowed vectors directly when the allow-list covers at most
VECTOR_INDEX_PREFILTER_MAX_SELECTIVITY(default 20%) of the index (#6502, #6514). - Builds use the machine. Graph construction is 93.4% of a DEEP-10M build and ran on
availableProcessors()/2threads with 31.04% of CPU burned inLongAdder.addon the distance path; striped counters cut 2-5x per lookup and the pool defaults to cores minus one, settable witharcadedb.vectorIndex.graphBuildParallelism(#5577). The location index went from ~90 to ~32 bytes per live vector (#5588). - Two silent wrong answers. A partial compaction of the sparse-vector index could permanently resurrect deleted documents or revert updates, because a merged segment got a globally new highest id and outranked a newer tombstone under “newest wins” (#6379); pre-fix merged segments are reported at index open. And a grouped search returned the groups with the lowest RIDs rather than the best-scoring ones (#5761, #6936).
openCypher: a Correctness Sweep, Then Neo4j Compatibility
A large batch of wrong-result defects came from differential fuzzing against Neo4j and Memgraph by @YGY-001 and @shulei5831sl, plus follow-ups. The shape is always the same: the identical query written two ways gives two answers.
- An edge variable read only inside a list predicate was anonymised, because the reference check scanned whitespace-stripped text instead of the AST, so the
WHEREread a missing binding and dropped every row (#6567, #6599); the same check had noCREATE/MERGEcase, soCREATE (c {since: r.since})wrote null (#6573). - Relationship uniqueness was scoped to one pattern part instead of the whole
MATCHclause, so the sameOPTIONAL MATCHreturned different row counts depending on whether the rows went throughcollect/UNWINDfirst (#6310). - A label disjunction
(y:A|B)on a node bound by expansion matched nothing because the target-side check ANDed the alternatives (#6338), a backticked label in aWHEREnever matched (#6345), andlabels()dropped a vertex’s own type under inheritance (#6363). MERGEcreated duplicates when the anchor vertex was bound earlier in the same query, breaking idempotency on the most common graph-building shape (#6461).- A standalone leading
OPTIONAL MATCHwith more than 100 matches never terminated, re-running its scan from scratch on every pull batch and emitting the first 100 rows for ever (#6668). MATCH p=(a)-[*1..N]->(b)materialized every path and exhausted a 512 MB heap on a modest fan-out graph where SQLTRAVERSEanswered in under a second; variable-length traversal is a lazy DFS generator now (#6097), and the cost-based optimizer plans it instead of falling back to the legacy executor (#5358).
On the compatibility side: 12 APOC-compatible functions and procedures including apoc.refactor.mergeNodes, apoc.refactor.cloneNodesWithRelationships and apoc.do.when (#6059, #6060, #6157); db.index.fulltext.queryNodes / queryRelationships bring BM25 full-text search into Cypher (#6729); and the Neo4j 5 dynamic-label syntax SET n:$(expr) / REMOVE n:$(expr) is implemented, where it used to parse and then create a vertex type literally named $(node.labels) (#7059, #7093).
GQL: Quantified Path Patterns, Phase B
Quantified Path Patterns beyond the single-relationship case (ISO/IEC 39075 §15.4) were rejected with FeatureNotImplemented. A parenthesised sub-pattern can now repeat with a quantifier, carry its own WHERE evaluated per repetition, bind group variables as LIST<NODE> / LIST<RELATIONSHIP>, and support grouped path assignment with relationship isomorphism enforced across the group (#4531).
Three pre-existing bugs were fixed on the way: the Phase A rewrite dropped an inner endpoint label and could return rows through wrongly-labelled nodes, an inner node’s inline WHERE could not see earlier bindings of the same repetition, and deep repetitions overflowed the stack at about 5,000 (they run iteratively to 20,000 now).
The Postgres Wire Protocol Works With the Defaults Your Driver Uses
Twenty-eight defects, most of them reported by driver behaviour rather than by reading the spec:
UPDATEon a vertex type in autocommit, the JDBC, psycopg and Spark default, failed withTransaction not activewhile document and edge updates succeeded. A vertex can be modified outside a transaction now, andUPDATE/DELETE/INSERTin autocommit each run as one statement-level transaction (#7096, open as discussion #1588 since 2024).- An error inside
BEGINwedged the session permanently:ReadyForQuerynever reported'E',COMMIT/ROLLBACKwere not recognised, and every further statement was silently swallowed (#6457, #6543). - JDBC fetch size silently truncated results to the first N rows:
PortalSuspendedwas written before the rows and the portal removed, so the follow-upExecutefound nothing (#6458). - pgjdbc’s sixth execution of a
PreparedStatementserved stale rows: re-Binding an already-executed named statement reused the portal without resettingexecuted(#6660). - Schema probes answered nothing.
WHERE 1=0andLIMIT 0over a computed projection returned noRowDescriptionat all, which is what Spark, Tableau and several JDBC and BI tools send first (#6156, #6185). - An idle connection busy-polled its socket ten times a second because
readMessage()never blocked, so N pooled connections cost 10N wakeups per second (#6410).
MongoDB, Bolt, gRPC, GraphQL, Redis and Gremlin got the same treatment; see Wire Protocols below.
Optional mTLS on the Raft Transport
The gRPC transport between cluster nodes (AppendEntries, RequestVote, snapshot transfer) ran in plaintext with no peer authentication, so any host that could reach the port could inject log entries. Optional mTLS is configurable through arcadedb.ha.tls.enabled, arcadedb.ha.tls.certChainFile, arcadedb.ha.tls.privateKeyFile, arcadedb.ha.tls.trustCertCollectionFile and arcadedb.ha.tls.mutualAuth (#3890).
Off by default. Startup fails fast if any PEM file is unreadable, mutualAuth=false gives server-only encryption, and the leader’s own Raft client plus the Kubernetes auto-join probe were fixed to carry the same TLS parameters instead of dialling in plaintext.
Certificates are read from disk at startup only, so rotating them requires a restart.
Security Advisories
This release closes six security advisories, each published in full (impact, affected versions and credit) as a GitHub Security Advisory on the repository. All six affect 26.8.1 and earlier and are patched in 26.9.1.
Per-type ACL enforcement
- GHSA-wjhv-79gv-2pqg (high): TimeSeries
typesACL entries were not enforced on the write paths. Reported by @FEARIS2. - GHSA-2c8m-q484-jv7m (high): index-target and TimeSeries reads, writes and counts reached records without the bucket-level permission check. Reported by @ruispereira.
- GHSA-27vw-j8qc-5h7x (medium): batch parallel-flush edge-connect writes ran on async workers with no principal bound, bypassing per-type ACLs. An incomplete fix of GHSA-c23x. Reported by @manus-use.
- GHSA-chrr-vr3p-crcc (medium): the AI Chat
query_databasetool bypassed per-type and per-bucket ACL enforcement. Reported by @T4ran24.
Untrusted input reaching the host
- GHSA-67m7-7w7g-mpmh (high): the
IMPORT DATABASESSRF guard did not extract IPv6 transition addresses (NAT64, 6to4, Teredo), so an internal IPv4 address could be reached through an IPv6 spelling. An incomplete fix of GHSA-4w2m. Reported by @tonghuaroot. - GHSA-j57p-qmrh-v7xv (high): the script-trigger sandbox’s
DENIEDentry forjava.util.ResourceBundlewas bypassed by its subclasses, allowing classpath credential disclosure. Reported by @baeseungwon1010.
Three further advisories were published just after the 26.8.1 release and are fixed in 26.8.1, not here: GHSA-rv64-62hr-wv2p (CVE-2026-76223), GHSA-wcm5-4wjm-9wj3 and GHSA-mmww-w3w3-6r86.
Also Hardened in This Release
Pre-authentication denial of service on the wire protocols. An unauthenticated client could exhaust the server with a handful of bytes on three protocols, all found by @ruispereira:
- The Bolt WebSocket transport sized a byte array from the client’s 64-bit frame length with no bound, so a ~14-byte frame declaring a 2 GB payload forced a 2 GB allocation before the handshake (#5894); the PackStream decoder did the same from a 32-bit length and recursed without a depth limit (#5918); and
LIST_8/LIST_16/MAP_16element counts bypassed those guards whileListFrameallocated eagerly, so a ~3 KB message could force ~256 MB of live heap (#6800). New bounds:arcadedb.bolt.websocket.maxFrameSize(16 MB),arcadedb.bolt.maxMessageSize(16 MB),arcadedb.bolt.packstream.maxValueLength,maxElements,maxDepth. - The Redis wrapper parsed RESP arrays with unbounded recursion and an unvalidated element count, so a ~47 KB message of nested arrays overflowed the stack with no credentials (#5895). New bounds:
arcadedb.redis.maxMultiBulkDepth(32),maxMultiBulkLength,maxBulkLength. - The Postgres handshake had no pre-authentication read timeout and accepted unbounded startup parameters, so a client that connected and sent nothing pinned a thread and a file descriptor for ever (#6377); the listener also accepted unbounded pre-auth connections (#6412). Redis and Bolt got the same window, and TCP keepalive is enabled on server sockets so a half-open connection is dropped by the OS rather than pinning a thread for ever (#6761).
Catastrophic regex backtracking. SQL MATCHES and openCypher =~ handed a user pattern straight to java.util.regex with no bound, so (.*a){20}$ on a 41-character string pinned a query thread indefinitely and arcadedb.command.timeout could not stop it. The new arcadedb.command.regexTimeout (default 1000 ms) bounds every regex evaluation independently of the command timeout (#5886). Parser recursion is bounded too, in Cypher, SQL and GraphQL (#5851, #5853).
Other hardening
restore database <url>followed redirects with no per-hop revalidation, so the one-shot host check was bypassed by a3xxredirect or DNS rebinding to an internal address (#6381). The two independent SSRF checks onimport databasealso read two different configuration keys, so the documented opt-out worked for only one of them (#6474).- Revoked database-level grants stayed in effect until restart. A group’s
updateSchema,updateSecurityandupdateDatabaseSettingsgrants and itsresultSetLimit/readTimeoutwere frozen at the values seen when the user first touched the database (#6806). - The polyglot (JS) engine kept script parameters bound in the shared context after each command, so a later
jscommand from any caller could read a previous caller’s parameters and globals (#6759), and the host-class allow-list’s ancestor walk skipped package-wildcardDENIEDentries (#6045). - Credentials stopped being written down. The console wrote every
connect remote: ... <password>andcreate user ... identified by <password>line to./.historyin cleartext and echoed it in-bmode (#6829), and witharcadedb.bolt.debug=truethe HELLO message logged the caller’s cleartext password (#6801). POST /api/v1/loginminted a session per call into an unbounded map, storing untruncated client-controlled headers for at least 30 minutes (#6809), and thedbtag of thearcadedb.http.requestsmeter was the raw path parameter with no existence check, so unauthenticated requests grew the meter registry without bound (#6805).- Bolt
LOGOFFwas accepted in any state and left the open result stream and explicit transaction alive on a now-unauthenticated connection, so a later user could commit writes made before the user change (#6803). - The Gremlin shaded jar bundled Jackson 2.15.2. TinkerPop’s
gremlin-shadedships its own relocated copy with the original version metadata, so Docker Scout and grype flagged it and GraphSON serialisation actually ran on it. The jar is rebuilt on the project-wide Jackson 2.22.2 (#7097). - WAL recovery allocated a page array straight from a file-read count, so a corrupt page-count field produced an
OutOfMemoryErrorthe recovery guard could not catch (#6932), and PromQLquery_rangeoverflowed its step-count guard and wedged an Undertow worker in an unbounded loop (#6807).
New Features
SQL
INSERT ... ON DUPLICATE KEY SKIP: a multi-record insert no longer aborts the whole batch on the first duplicate key. Records violating a unique index are skipped and reported with@skipped: true, the offending index and the key; works withCONTENT,SETandINSERT ... FROM <query>(#4918).CHECK DATABASE FIX RECLAIM UNREFERENCED FILESdeletes files with no schema component, left behind by an abandoned HA schema instalment sequence, and reports them (#6189).CHECK DATABASE ... DEEPis a new tier for the expensive TimeSeries sealed-store checks, with aFIXarm that repairs what is derived from the sealed blocks (#6360).SQLFunction#isDeterministic()lets a function opt into plan caching and constant folding;abs,pow,sqrt,coalesce,ifnull,ifempty,if,decodeandstrcmpcido (#6190).
Query Languages
- GQL Quantified Path Patterns Phase B, see the highlight above (#4531).
- 12 APOC-compatible Cypher functions and procedures:
coll.sum,coll.avg,coll.union,coll.unionAll,coll.toSet,coll.pairsMin,math.round,convert.toString,number.format,apoc.do.when,apoc.refactor.mergeNodesandapoc.refactor.cloneNodesWithRelationships. db.index.fulltext.queryNodes/db.index.fulltext.queryRelationshipsYIELD(node|relationship, score)from ArcadeDB’s BM25FULL_TEXTindex inside a Cypher statement, matching Neo4j (#6729).- Cypher dynamic labels
SET n:$(expr)andREMOVE n:$(expr), plusREMOVE n IS Label. - The GQL standalone
FILTERclause actually filters (#6574).
Server and Operations
- Optional mTLS on the Raft gRPC transport, see the highlight above (#3890).
- A per-protocol HA routing table.
getRoutingTable(ROUTING_PROTOCOL)and agrpc:field inarcadedb.ha.serverList, so a follower refusinggraphBatchLoadcan name a dialable gRPC address in thearcadedb-leader-grpc-addresstrailer instead of only the leader’s HTTP address (#6091). /api/v1/clusterreports live Raft membership. Every peer carriesinConfiguration, a declared peer the cluster no longer contains reports roleNOT_IN_CONFIGURATION, the divergence raisespeers-not-in-configuration/peers-not-in-server-listalerts, and Studio shows the state (#7040).- A skip mode for the bulk importer.
-onRowError skip|abort(defaultabort) logs and skips a malformed or out-of-range row instead of aborting the whole job, counting it in the summary (#5968). - Exponential backoff with full jitter for transaction retries, starting from the new
arcadedb.txRetryDelayBaseand doubling per attempt up to thearcadedb.txRetryDelaycap, instead of drawing from the same flat window on every attempt (#5587). - The OpenAPI spec is a publishable, self-identifying contract, smoke-tested against a TypeScript client generated from the server built in the same commit (#4894).
Major Fixes and Improvements
Storage and Integrity
CHECK DATABASEat scale. A hub vertex’s adjacency list was re-walked once per edge, O(degree²) on super-nodes: oneCHECK DATABASE FIXon a real 657 GB graph measured 80h19m. A per-pass probe cache takes that to O(degree) (#6062). Orphan edge records are named and reclaimed (#6090), and repairs are budgeted and committed in batches instead of stopping (#6320).- A fenced database no longer hangs with “No flush progress for 60000 ms”, reported twice from production during a
GraphBatchimport and aDELETE ... BATCHloop. A database fenced after a failed post-WAL commit stranded queued page-flush acks (#6505). - Renaming a vertex type broke every subsequent edge insert on that type with
SchemaException: Bucket with name 'Human_0_out_edges' was not found, on both 26.7.2 and 26.8.1, because the edge-chunk bucket and file names were derived wrongly and mangled further on every rename (#6667). - A forward bucket scan fetched one page past the end, synthesising a phantom zero-filled page and inserting it into the global read cache, and a record that failed to materialise during a scan was logged and silently dropped from the result set (#6014, #6015).
removeSuperType()withdrew only the type’s own buckets from the ancestor’s polymorphic cache whilelinkSuperType()had contributed the whole subtree, so after unlinking B from A a grandchild’s records still came back fromSELECT FROM A(#6935).- A date pattern with
MMM/EEErendered month names in the JVM default locale, so a schema date written on anit_ITnode failed to parse on another, andFileUtils.copyFileignoredtransferTo’s return value so a file over 2 GB was silently truncated (#7112).
Indexes
CREATE INDEX <name> IF NOT EXISTSansweredcreated: trueunder the requested name while silently reusing a pre-existing index on the same property, soSEARCH_INDEX('<name>', :q)later failed or ranked nothing (#6921).- Index configuration is no longer lost on the repair and restore paths.
TRUNCATE TYPE,CHECK DATABASE FIX, adding a bucket and adding a supertype recreatedFULL_TEXT, geospatial andLSM_SPARSE_VECTORindexes from the underlying LSM-Tree’s metadata, so analyzers, BM25 parameters, geohash resolution and sparse-vector settings silently reverted to defaults (#5742, #5934). REBUILD INDEXno longer returns silently when it fails. Both it andCHECK DATABASE FIXretried the whole drop-and-create body, so a failure after the drop had committed could leave the index permanently missing (#6040).CONTAINSTEXTon a single-property full-text index split its literal on:, so any value containing a colon returned no matches (#6382); twoCONTAINSTEXTconditions on the same property sent only the first to the index (#6427); and a field-qualified phrase query ignored its field (#7000).- An
LSM_TREEindex stores aLINKkey as a compressed RID of about 2-7 bytes instead of a fixed 12 per column, roughly halving the key bytes of an(@out, @in)edge de-duplication index (#5703). - An index cursor allocates 6-8 fewer short-lived objects per row on a unique-index range scan, about 7M objects saved on a 1M-row scan (#6944).
Numeric Correctness
A family of unchecked narrowings, all found by @ruispereira, all silent:
- Storing an out-of-range value in an
INTEGER/SHORT/BYTEproperty wrapped it:SET n = 3000000000stored-1294967296with no error (#5905). SUM()/AVG()over anINTEGERcolumn overflowed silently once the running sum passedInteger.MAX_VALUE: five rows of 2,000,000,000 gavesum = 5705032704instead of 10,000,000,000 (#5906).LIMIT 2147483648narrowed toInteger.MIN_VALUEand returned 0 rows, a finitedoubleaboveFloat.MAX_VALUEwas dropped from map JSON, and aDOUBLEMIN/MAX constraint was checked asfloat(#5919).BinaryComparatornarrowed the wider operand to the first operand’s width, giving a non-antisymmetric order, and parsed string operands withInteger.parseInt, soWHERE n < 'abc'crashed (#5900).NaNnarrowed to0when converting aDouble/Floatto an integral type, scalar and array paths alike (#5970, #6020).
SQL
split()returns aString[], and the operator surface now handles it.CONTAINSon either side,CONTAINSANY, andjoin()/sort()/first()/last()/asList()all mishandled a plain array:'a b c'.split(' ') CONTAINS 'a'was false,join()leaked[Ljava.lang.String;@7a8fa663,sort()returned the input unsorted (#6984, #7084).sum/avg/min/maxover zero matching rows returned an empty result set whilecount(*)returned one row with 0; they return one null row now, per ANSI SQL (#6680).astar()computed every heuristic cost as if the node were the start, so A* anddijkstra()with axis coordinates could return non-optimal paths (#6385).- 62 SQL functions and methods threw raw JDK exceptions on missing, negative or wrong-typed arguments (
'abc'.substring(),left('abc', -1),range([1], 3)); arguments are validated and reported as HTTP 400 client errors now (#5884, #5885). arcadedb.command.timeoutnow bounds what it claims to. The deadline belongs to theCommandContext, inherited by subqueries, UNION branches and parallel scan workers, and is checked inside openCypher scans, expansions and joins, SQLTRAVERSE/MATCH/filter steps, pathfinding functions, WHERE-less aggregation scans and the vector k-NN path (#6266, #6873).- Schema probes are free.
WHERE 1=0andLIMIT 0fold to anEMPTY RESULTstep at plan time instead of scanning the target, andWHERE 1=1folds away instead of being evaluated per record (#6174, #6184). SELECT FROM $varmutated the cached statement’s target in place, so a later execution of the same SQL text with a different binding read the first execution’s type (#6669).- A property
DEFAULTthat failed to parse was silently stored as its own source text on every record, and re-parsed on every insert; defaults are validated at DDL time and parsed once (#6134).
Graph Engine and Analytical Views
- A Graph Analytical View’s delta overlay is deletion-aware. Deleting one of several parallel edges masked all of them (#6769), an edge created and deleted inside the same overlay window still surfaced as live (#6775), and after a base vertex was deleted the dense node ids could exceed
getNodeCount(), so everyalgo.*procedure silently skipped live vertices (#6792). - Super-node edge ordering. Once a vertex crossed 4,096 edges its iteration order was silently replaced by the concatenation of 16 hash-striped chains, seen in production as newly created records vanishing from a “newest 100” listing; the stripes are interleaved approximately newest-first now, with
arcadedb.graph.supernodeInterleaveRoundsdegrading to plain concatenation for a full walk (#6044, #6064). - All 22 superlinear
algo.*procedures are abortable and budgeted. An O(V³) run the memory budget admitted ignoredThread.interrupt(),arcadedb.command.timeoutand client cancellation (#6302); the graph analgo.*call loads, the embedding matrices and thenodeCount²bitsets are all priced againstarcadedb.cypher.algoMaxWorkingMemorynow (#6317). - Two
algo.*wrong answers:algo.steinerTreeandalgo.maxKCutpaired edge weights with neighbours by iteration position, so arelTypesfilter or the mere presence of a Graph Analytical View produced wrong trees, weights and partitions (totalWeight1000.0 for a tree costing 2.0) (#6301, #6376).algo.wccignored itsrelTypesargument andalgo.degreeignored itsdirection.
Bulk Load and the Async Executor
- A JSONL batch load silently dropped vertices. 19,484,584 vertex lines in the file, about 17.2 million created, then “Unknown temporary ID” when an edge referenced one of the missing ones (#5618).
GraphBatchretained 16-18 GB of caches for 100M distinct vertices, forcing a 663M-vertex, 14B-edge gRPC load stream to be recycled every 4M records to keep the server alive; the caches are bounded and deferred incoming edges drained early (#5664).- The async worker pool stopped being torn down and respawned.
setTransactionUseWAL()/setTransactionSync()recreated the whole pool, four times perGraphBatchflush, force-exiting every other user’s queued tasks (2,183InterruptedIOExceptions in one production log); the flags are plain volatile writes now, andsetParallelLevel()resizes in place (#6509, #5665). - Async writes behave like synchronous ones:
updateRecord()never calledvalidate()(#7002),deleteRecord()fired every before- and after-delete listener twice (#7003), andscanType()returned normally when a bucket scan threw (#6467). - A truncated batch upload applied its records twice, the 409 being the duplicate-key mapping, so a client resuming from the reported counts double-inserted; the interrupted stream commits once and answers 408 with the real counts (#6176).
Backup, Export and Import
- JSONL export and import lost data silently. The exporter wrote DATE values as epoch milliseconds while the importer decoded them as epoch days, so every record with a modern DATE was dropped on import together with its edges (#6455); LINK property values were never remapped, so restored links pointed at unrelated records (#6460); and both sides logged per-record failures and reported success (#6468, #6471).
- Two concurrent backups of the same database wrote the same second-precision path, producing a torn or overwritten archive; backups are serialised per database and the target path claimed atomically (#6753). An auto-backup schedule was never cancelled when its database was dropped or closed (#6752).
- The OrientDB importer parsed every JSON number as
double, so LONG values above 2⁵³ were off by one, and it silently dropped composite indexes, losing UNIQUE constraints after migration (#6749, #6750). - Importing any ZIP source silently yielded 0 records and reported success (#6810), and a user-supplied CSV delimiter was overwritten with null (#6811).
HA and Raft Clustering
- A replica-originated insert lost its unique-index entry on every node. The record committed and replicated cluster-wide, a full scan found it,
lookupByKeynever did, and a duplicate key could be inserted, because a replica committing its own transaction shipped only the record data (#6964). - A large, highly compressible bulk transaction crash-looped an entire cluster. It passed the 32 MB submit-time gate measured on the compressed envelope, but its 77,158,147-byte uncompressed WAL exceeded the 64 MB decode ceiling, so every node of a 4/5-node cluster crashed at the same Raft log index on every restart (#5933).
- A snapshot install that gave up on one database still ACKed for all of them, cleared the stale-read floor and let Ratis purge the log, so LINEARIZABLE and read-your-writes reads of the stale database were served from stale state (#6760).
- REST user management did not replicate.
POST/PUT/DELETE /api/v1/server/usersmutated the user store only on the node that served the request, while the equivalentcreate usercommand replicated through Raft, so a user created via REST got 401 on the other nodes (#6808). RaftGroupCommitterawaited each entry of a batch with the full quorum timeout sequentially on one thread, so an unresponsive quorum stalled replication for about 83 minutes at defaults (#5848).- A follower whose log writer hit
No space left on devicestayed RUNNING while rejecting every append, with nothing short of an operator restart recovering it; the health monitor restarts the server in place once the volume has room (#7037). - Self-dial loops on single-host clusters are closed. A follower whose derived leader address resolved to itself forwarded every write to itself in an unbounded loop (#6191),
localhostand127.0.0.1were not recognised as the same endpoint (#6204), andverifycould fan out to itself and reportALL_CONSISTENT(#6221). CHECK DATABASE FIXworks on a cluster. Each per-type repair used to be one unsplittable Raft log entry that aborted at commit on a large database; repairs ship as bounded instalments now (#6128).
TimeSeries
- Bucketed aggregation silently dropped everything appended since the last compaction, because it sized its bucket array from the sealed stores only, so any dashboard query over the newest data was wrong (#6937).
- A TimeSeries type whose sealed store failed to load disappeared from the schema and the database opened as if it had never existed, with the next write creating a fresh empty type; it is registered with its engine unavailable and fails loudly by name now (#6356).
- Under HA, a shard whose sealed store grew past 48 MB stopped sealing for ever, so its samples stayed uncompressed in the mutable bucket with no retention or downsampling; an oversized store ships as ordered slices that followers reassemble and verify, raising the ceiling from 48 MB to roughly 2 GB (#4416).
- PromQL fixes:
orreturnedNaNwhenever both sides shared a label set, label matchers on an absent column matched backwards, range points were not step-aligned (#6938), andmin_over_time/max_over_timereturned±Infinityfor an all-NaN window (#7039).
Wire Protocols
- MongoDB.
findOne/updateOne/deleteOneby ObjectId_idnever matched (#6745);skipandsortwere silently ignored (#6746, #6747);$exists: falsereturned the documents that had the field (#6748); an upsert filtered on_iddiscarded it and created a duplicate on every call (#6940); and{field: null}matched nothing (#6952). - Bolt. A second
RUNinside an explicit transaction while the first result stream was open, the normal shape with a driver fetch size smaller than the row count, was rejected as a protocol error (#6804); a property declaredARRAY_OF_FLOATSread back as[F@294b13ceinstead of a list, so any client reading embeddings over Bolt got a corrupted string (#7056); and a fragmented WebSocket message had its continuation frames discarded (#6802). - gRPC.
insertStreamandbulkInsertignored the caller’sTransactionContextand committed on their own, so rows survived a subsequent rollback (#6607); errors were flattened tosuccess=falseso the client lost the exception type and never retried a conflict (#6192); and a stream longer thantxMaxIdleMswas reaped mid-stream and its rows lost (#6755). - GraphQL. Variables were accepted by the parser but always resolved to null and interpolated into the generated SQL as the literal
null(#6834); the standardquery($a: String, $b: Int)failed to parse because the comma was a real token (#6860); and field aliases threw an NPE (#6384). - Redis. Bulk strings were read byte-by-byte as
(char) b, mangling any non-ASCII payload (#5907); a RESP2 null bulk string consumed two extra wire bytes and desynced the connection (#5911); andSETignored all its options (#6466). - Gremlin.
ArcadeGraph.close()committed an open transaction instead of rolling it back, so an aborted unit of work became durable, and a pooled graph was returned to the factory with its transaction still open so the next borrower inherited and committed another caller’s writes (#6820, #6821). Thearcadedb-gremlincoordinate was unusable standalone in 26.8.1 (#5879).
Server, HTTP and Console
- A single HTTP response is bounded by a ceiling no caller can widen.
httpQueryDefaultLimitprotected only callers that stated no limit, soLIMIT 100000000or"limit": -1made the server serialise an unbounded result into one JSON response; the newarcadedb.server.httpQueryMaxResultRows(default 1,000,000) refuses with HTTP 413 rather than truncating (#5719). - The remote client’s watchdog fired after 8h20m instead of 30s, multiplying the millisecond socket timeout by 1000 (#5847), and
RemoteGraphBatch.flush()left the payload buffered on failure soclose()re-sent it and duplicated committed records (#7031). - The
@propstype hint leaked into every response. It appeared in HTTP JSON results for non-element rows, intoJSON(true)and in WebSocket change events broadcast to every subscriber; it is opt-in now through atypeHintsrequest flag, which the Java driver sets automatically (#5812). - The console dropped every unescaped backslash before the command reached the engine, so a Windows path or a regex literal could not be typed, passed with
-b, or replayed withload(#6827); andconnect remote:failed on a password containing a space (#6830). - Kubernetes and Docker quickstarts that could not work. The StatefulSet example used
${VAR}incommand:, which is never expanded, so the root password became the literal${rootPassword}and every pod claimed peer name${HOSTNAME}, and it wired HA on 2424 while Raft binds 2434 (#6840). The Docker image pinned-Xms2G -Xmx2G, sodocker run -m 512mdied at startup (#6841).
Upgrade Checklist
No schema migration is required and no existing database is rewritten, but this release contains behaviour changes. Five minutes of checking before you upgrade:
- Back up first (it is 27.6x faster now), then read the breaking changes in full.
- Rolling HA upgrade: upgrade followers before, or together with, the leader. A node running an older build cannot install a sliced TimeSeries sealed store (#4416).
- Grep your schema scripts for out-of-range integral literals.
SET n = 3000000000on anINTEGERproperty now raises a validation error where it used to store-1294967296. A bulk import carrying such values will surface the error; use the new-onRowError skipto continue past them. - Check any HTTP caller relying on
limit: -1or a hugeLIMITto fetch more than 1,000,000 rows in one response: it now gets HTTP 413. Raisearcadedb.server.httpQueryMaxResultRowsor set it to-1. - Check any client reading
@propsout of HTTP JSON,toJSON(true)or WebSocket change events: it is opt-in through thetypeHintsrequest flag now. - After the upgrade, rebuild two things.
LSM_TREEindexes written before the #5321 comparator change should be rebuilt; the condition is reported once per logical index as a queryable upgrade warning, visible throughschema:indexesand Studio, naming theREBUILD INDEXto run (#5802). RunCHECK DATABASE FIXonce to sweep the orphaned chunk slots this release learned to reclaim. - If archive size matters more than backup time, set
arcadedb.backup.compressionLevel=9to keep the old ratio. - On openCypher, note that “no labels” is a reserved sentinel type
~NO_LABEL~:VandVertexare ordinary labels now, solabels(n)on a vertex whose only label wasVchanges on pre-26.9.1 data (#6395).
Dependency Updates
Around 120 dependency bumps landed in this cycle, almost all through Dependabot. The notable ones: the Gremlin shaded jar rebuilt on the project-wide Jackson 2.22.2 so it no longer bundles TinkerPop’s relocated Jackson 2.15.2, Ratis 3.3.0, Netty 4.2.17.Final, Undertow 2.4.3.Final, Lucene 10.5.1, protobuf-java 4.36.0, Logback 1.6.3, snakeyaml 2.7, JLine 4.4.0, Micrometer 1.17.1, OpenTelemetry 1.65.0, Jedis 8.0.1 and the Neo4j Java driver 6.2.1. The Gremlin ANTLR runtime and the Groovy major remain deliberately frozen, as TinkerPop cannot take a newer one.
Getting Started with 26.9.1
Docker
docker pull arcadedata/arcadedb:26.9.1
Visit our Docker Hub repository for more information.
Maven
<dependency>
<groupId>com.arcadedb</groupId>
<artifactId>arcadedb-engine</artifactId>
<version>26.9.1</version>
</dependency>
All artifacts are available on Maven Central.
Documentation
For details on features and usage, see the documentation.
Compatibility Note
This release requires no schema migration and rewrites no existing database, so no export or import is needed when upgrading. It does contain behaviour changes, collected in the upgrade checklist above and in full in the release notes. As always, we recommend creating a database backup before upgrading.
Download ArcadeDB 26.9.1 now: GitHub Releases
Thanks to everyone who reported, reproduced, reviewed, tested and fixed, in particular @232-323, @7487, @ajinsads, @altugsogutoglu, @baeseungwon1010, @borutjures, @cakeni, @chow8386, @danieljuhl, @dmoree, @EQSTLab, @FEARIS2, @g33kroid, @gramian, @GYWang1983, @ivan-velikanov, @jjj-n, @josh1e, @justinblethrow-cloud, @kl-demi, @leanworld7-netizen, @LepsyMikolaj3301, @lohithsamaga, @manus-use, @mdre, @NooriUta, @odysseaspenta, @ruispereira, @ruslan-butyk-fntext, @sbsrouteur, @shulei5831sl, @syntact-io-office-user, @T4ran24, @tae898, @tobiasdam, @TobiasJoseHermann, @tonghuaroot, @waterWang, @YGY-001 and @ZwaarContrast.
Luca Garulli ArcadeDB Founder