ArcadeDB 26.9.1
Overview
This is the largest release ArcadeDB has ever shipped: 992 issues and pull requests closed under the 26.9.1 milestone - 675 issues and 317 pull requests - out of 655 pull requests merged and 1,500 commits in total since 26.8.1.
The headline work is in five places:
- Backup and restore - a full backup is 27.6x faster, a restore runs in parallel, and neither freezes page flushing any more, so writers keep working while a backup runs.
- Storage integrity - the family of defects around records that outgrow a page is closed: silent lost updates, a record returned twice by a scan, false conflicts, and a 16% chunk-slot space leak that nothing ever reclaimed.
- Query correctness -
NOT INon an indexed property returned theINresult set,DISTINCT ... ORDER BY ... LIMITreturned too few rows, a multi-keyGROUP BYgrouped on the last key only, and an index range scan could return rows theWHEREclause excluded. All fixed, with regression tests. - Indexes are used where they were not - a literal
IN (...)list, a composite-index prefix withORDER BY,BETWEEN,@rid IN [...]and a Cypher label disjunction all take the index now instead of a full scan. - Vector search - opening a database is constant-time again regardless of index size, rebuilds are scheduled instead of blocking the first query, and recall stopped degrading past 10,000 vectors.
Upgrading is strongly recommended for every deployment. There are breaking changes and behaviour changes - they are collected in Breaking changes and upgrade notes and each is called out again where it belongs. No schema migration is required, and no existing database is rewritten.
Contents: Highlights · Security advisories · New features · Major fixes and improvements · Breaking changes and upgrade notes · Dependency updates
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 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 now 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 now per-database.
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
ConcurrentModificationExceptionwith 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 now 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) - andDISTINCTwas 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, #6482) |
Cypher MATCH (e:Child) WHERE e.id IN $ids with the 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 now materialised on first use (#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 now persisted at clean close with a freshness certificate and restored lazily (#6583, #6632, #6641). - 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 now reused as a prefix and only the gap is built, on the search path, the inactivity timer and async rebuilds alike (#6655, #6798, #6859). A single insert into a settled 50,000-vector index no longer triggers an 8-14 s rebuild 15 s later (#6496), the rebuild threshold scales past 250,000 vectors where the linear delta scan was ~79% of query time (#6797), and a graph that left any node unreachable no longer marks the index dirty for ever (#6489).
- 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 now widens with graph size, 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 now 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, #6600); 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 write left the row's other aliases pointing at a deleted record, so
REMOVE n:l1afterOPTIONAL MATCHfailed withRecordNotFoundException(#6312, #6313, #7022, #6977). - 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 andNOT (n:Tag)passed every row (#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).
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, #6843).
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 now run iteratively to 20,000).
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 now be modified outside a transaction, 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, #6545, #6548). - 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/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 and GraphQL got the same treatment - see Wire protocols.
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 now 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, reported by @manus-use), GHSA-wcm5-4wjm-9wj3 (reported by @chow8386) and GHSA-mmww-w3w3-6r86 (reported by @EQSTLab and @232-323).
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 (#5912) and Bolt (#5978) got the same window, and TCP keepalive is now 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, whileimport databasewas already hardened (#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(#6059, #6060, #6157). 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(#7059, #7093). - 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).
- New observability:
arcadedb.ha.schema.instalments,arcadedb.ha.schema.instalment_time_ms,arcadedb.ha.schema.instalment_max_time_msandarcadedb.ha.schema.unreferenced_filesper database (#6143, #6144), page-snapshot metrics (#6116), and vector-indexgetUpgradeWarning()/untrustedSegments/ deferred-rebuild state (#6566, #6657).
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 now named and reclaimed (#6090), repairs are budgeted and committed in batches instead of stopping (#6320), and a repair pass that throws gives back the transaction it opened (#6342).- 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 failed rename now rolls its already-renamed indexes back too (#6103). - 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 misaligned read no longer answers with invented property names pulled from the dictionary (#5774),
reload()on anImmutableVertexre-parses its edge pointers (#5771), and anAfterRecordReadListenerreturning a modified record with anEXTERNALproperty no longer creates, updates or deletes records in the external bucket during a plain read (#5770). - Bucket and type names containing Windows-illegal characters or reserved device names are rejected at validation time instead of failing later with a raw
IOException(#6104). - 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 INDEXdropped a named index's logical name (#5791); and JSONL import rebuilt them without their metadata (#5650). 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).- An indexed range query with a bound of another type (
WHERE n < '15'on an indexedINTEGERcolumn) threwClassCastExceptionwhile the un-indexed equivalent worked (#5932). 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); a BM25 conjunction over a multi-property index ran one full scoring pass per property (#6436); 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). - A partitioned subtype with a different bucket count crashed on its first indexed insert (#5645), a
DROP INDEXleaving a partitioned type without its partition index is reported at commit rather than at the next open (#5646), and removing a bucket re-binds the bucket-selection strategy (#6380). - 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).
CollectionUtils.compare(Map, Map)returned 1 in both directions for maps with disjoint keys, breaking the antisymmetryBinaryComparatorneeds to order index entries (#7111).
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). A STRING compared against a DATE/DATETIME was compared lexicographically (#5947, #5956).NaNnarrowed to0when converting aDouble/Floatto an integral type, scalar and array paths alike (#5970, #6020), and Cypher procedure options narrowed user numbers with no range check across 35 files (#5924).
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, #6995, #7084, #7027).sum/avg/min/maxover zero matching rows returned an empty result set whilecount(*)returned one row with 0; they now return one null row, 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),date.truncate('hour', time(...))); arguments are validated and reported as HTTP 400 client errors now (#5884, #5885, #5910, #6387-#6390, #6608, #6609, #6638, #6677). 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, #6459, #6465, #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), and both plan caches now check-and-insert atomically so a plan built before a concurrentDROP INDEXcannot be stored and reused (#6671).Result.toJSON()rendered every embedded document in a projection asnull(#6945), andMatchStatement.toString()never renderedNOT {...}patterns, so a materialized view round-tripped through its text silently lost its negative filter (#6999).- 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).DROP PROPERTYon a property with aDEFAULTleft its name in the type's cache, so every later insert failed withSchemaException: Cannot find property(#6799). - The native
select()builder applied the resultlimitto the index candidate scan, soIS NOT NULLwith paging returned 101 of 500 rows (#6565); treatedORwith aneq/like/ilikeleaf as fully indexed, returning 0 rows instead of 1,000 (#6577); droppedtimeout()for every index-answered plan (#6815, #6816, #6880); and could not round-trip its own JSON (#6817).
openCypher
Beyond the highlight above:
- The cost-based optimizer now plans variable-length paths. Any statement containing one used to be excluded from physical-plan execution entirely and fell back to the legacy executor; a native
VarLengthExpandoperator brings anchor selection, filter push-down and join ordering to them (#5358). - Count push-downs stopped over- and under-counting. A star-join
count(*)ignored the arms' endpoint labels (#6337) and inline property filters or dynamic labels (#6431); a two-patternMATCHsharing two labelled variables returned 0 rows (#6322); an unlabelled anchor could not be counted at all (#5757); a correlatedCOUNT { }body lost the push-down and materialised one row per edge (#5758); and everyCountOpsized its arrays from the live node count rather than the dense node-id upper bound, skipping or overflowing nodes when an overlay was active (#6967, #6943, #6992). - A subquery body referencing an outer relationship variable matched nothing (#5696), a disconnected
MATCHafter a relationship pattern bound null instead of producing the Cartesian product (#5810), andEXISTS { }silently accepted a variable a precedingWITHhad dropped from scope (#5825). DISTINCTandUNIONtreated1and1.0as distinct although1 = 1.0is true (#5789, #6676), built their dedup key by string-concatenatingname=value|so values containing=or|could collapse (#6540), and included unused path and anonymous pattern variables in the key (#6488, #6541).shortestPath()andallShortestPaths()ignored the pattern's hop bounds, soshortestPath((s)-[:R*..2]-(e))returned a 4-hop path (#7009, #7017).AND/ORdid not short-circuit, sofalse AND EsurfacedE's runtime error (#5835), and the lexer's error listener was never attached, so an unterminated string or bad escape was printed to stderr and dropped while the rest parsed as a different query (#5958).MERGE ... ON CREATE SET/ON MATCH SETused a private re-implementation of SET that handled onlyvariable.property, silently dropping dynamic keys and expression targets, andSET n = mwith an entity on the right was a no-op (#6831, #6832).- All 22 superlinear
algo.*procedures are now abortable and budgeted. An O(V³) run the memory budget admitted ignoredThread.interrupt(),arcadedb.command.timeoutand client cancellation (#6302, #6295, #6318); the graph analgo.*call loads, the embedding matrices and thenodeCount²bitsets are all priced againstarcadedb.cypher.algoMaxWorkingMemorynow (#6317, #6263, #6300, #6375); andalgo.apspstreams its up to n²-n rows instead of materialising them (#6296). - 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 (#6699) andalgo.degreeignored itsdirection(#6716).
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), RID-keyed deletion dedup could drop the deletion of an unrelated edge that reused a freed slot (#6777), 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, #6048, #6064). - A missing vertex reached through an edge list is reported as not-found, not as a retryable conflict that a single-threaded cleanup job would retry for ever (#6572, #6586).
GraphEngine.moveEdgedeleted the old edge record physically without removing its index entries or external values, staying correct only by slot reuse (#5779).- With a view registered, a
WHERE a <> cinequality between two nodes of a MATCH chain was silently dropped because entities from the view were compared by value rather than identity (#6010), and the SQL MATCH expand-into fast path asked for untyped forward-only connectivity, so a typed pattern edge matched any edge (#6670).
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). It also threw on any vertex already promoted to the super-node layout (#5667).- 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, #6526). - Async writes now behave like synchronous ones:
updateRecord()never calledvalidate()(#7002),deleteRecord()fired every before- and after-delete listener twice (#7003),onOkfired before the batch that could still roll the record back (#6470),scanType()returned normally when a bucket scan threw (#6467), and a quiesce-based index build could miss a pending in-edge or scan while a retired worker was still writing (#6462, #6534). - A truncated batch upload applied its records twice - the 409 was 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, #6180).
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). A database containing a TimeSeries type aborted the round trip entirely (#7032).
- 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, #5889). 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), a user-supplied CSV delimiter was overwritten with null (#6811), a record logged as skipped was saved as an anonymous document anyway (#6812), an empty XML sub-element inherited its sibling's text (#6813), and the CSV type analyzer typed a column
LONGon the strength of a value it never examined (#6814).
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, #6111).
- 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); theQuorum.ALLwatch loop had the same shape (#6373).- 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 now restarts the server in place once the volume has room, and a snapshot install purges the local Raft log first (#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);verifycould fan out to itself and reportALL_CONSISTENT(#6221); and a Ratis-initiated snapshot install could resync a node from its own address (#6202). - Bootstrap file-id divergence could let a replicated schema change reuse a file id already in use on one node, silently dropping the new component (#6063, #6124), and a cold-boot bootstrap election could refuse the leader's own copy of a database it had just created (#7011).
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 now ship as bounded instalments, with the index rebuild no longer buffering its whole WAL in leader heap while stalling every other writer (#6128, #6136).- A leaked thread per stop/start cycle in
SnapshotHttpHandler,PostVerifyDatabaseHandlerandHAReplicationMetrics(#5890, #5850), and a Kubernetes auto-join probe with aretryForeverNoSleeppolicy that spun forever and wrote a 41 GB log (#5973).
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 now registered with its engine unavailable and fails loudly by name (#6356), it can be re-initialised on demand, and the Raft apply path installs the leader-shipped sealed blob that repairs it (#6839).
- 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 now ships as ordered slices that followers reassemble and verify, raising the ceiling from 48 MB to roughly 2 GB (#4416, #6933).
- 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±InfinityorDouble.MAX_VALUEfor an all-NaN window (#7039, #7043). ts.timeBucket()truncated toward zero, so a pre-1970 timestamp got a bucket start after the input (#6824), and a component could be handed a file whose id was not its own or read at the wrong stride after reopen (#6283, #6314).
Wire protocols
- MongoDB.
findOne/updateOne/deleteOneby ObjectId_idnever matched (#6745);skipandsortwere silently ignored (#6746, #6747);$exists: falsereturned the documents that had the field, and$notemitted invalid SQL (#6748); one document with a non-hex_idmade every subsequent read of the collection throw (#6939); an upsert filtered on_iddiscarded it and created a duplicate on every call (#6940);insertreportedn + 1andcountignored its query (#6941); 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); a fragmented WebSocket message had its continuation frames discarded (#6802); andCALL merge.relationship(...)failed with "Transaction not active" while the same Cypher worked over HTTP (#6547). - gRPC.
insertStreamandbulkInsertignored the caller'sTransactionContextand committed on their own, so rows survived a subsequent rollback (#6607);lookupByRID()threwRecordNotFoundExceptionfor a vertex a SQL query over the same connection found (#6404); errors were flattened tosuccess=falseso the client lost the exception type and never retried a conflict (#6192); a stream longer thantxMaxIdleMswas reaped mid-stream and its rows lost (#6755);batch()silently sent JSONL over HTTP instead of the streaming RPC (#6070); andStreamQueryin PAGED mode discarded the caller's ownORDER BY(#7029). - GraphQL. Variables were accepted by the parser but always resolved to null and were 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);Booleanargument values were always null andFloatvalues were passed as strings (#6383); field aliases threw an NPE (#6384, #6453, #7036); nested selections resolved directives against the top-level type (#6833); and string escapes were never decoded (#6836). - 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);PING <message>replied with a simple string, so a CRLF payload split the reply and permanently desynchronised it (#6942);SETignored all its options andINCRBYparsed a 32-bit amount (#6466); and error replies were always prefixed-ERReven forWRONGPASS/NOAUTH/NOPERM(#6560). - 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). ARemoteDatabase-backed traversal leaked a Netty event-loop group per graph instance (#6822), thearcadedb-gremlincoordinate was unusable standalone in 26.8.1 (#5879, #5937), and the shaded jar's bundled dependencies are now relocated or excluded so they no longer collide with a consumer's own Groovy or Lucene (#6771, #6793).
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). - A retryable conflict on
POST /api/v1/commandanswered 500 becauseDatabaseAbstractHandlerwrapped every handler exception in aTransactionException(#6201); a database closed by a resync answers a retryable 503 that the remote client retries transparently, while a permanent close answers accurately (#6776, #6778). - The remote client's watchdog fired after 8h20m instead of 30s, multiplying the millisecond socket timeout by 1000 (#5847).
RemoteDatabase.transaction()retried a transaction the caller had joined and never rolled back a failed attempt (#7030), andRemoteGraphBatch.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, #5863). - The shutdown hook applied its 2000 ms STARTING bound to databases that were already open, so a container killed mid-start closed them uncleanly and replayed the WAL on every restart (#7025).
- 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); an unterminated{swallowed every following;-separated command (#6392, #6439);connect remote:failed on a password containing a space (#6830); andclose()could lose buffered output (#6828). - 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), and the README and compose quickstarts passed settings viaJAVA_OPTS, silently replacing the image's ZGC flags with G1 (#6842). - MCP:
full_text_searchhad no upper bound onlimit,set_server_settingaccepted an unparseable value for a typed setting and returned success, and the registry manifest pinned an image six releases behind (#6837, #6875, #6838).
Breaking changes and upgrade notes
No schema migration is required and no existing database is rewritten. The items below change behaviour that existing code or scripts may depend on.
Java API
GraphTraversalProvider.getEdgeProperty(nodeId, neighborIndex, direction, edgeType, property)was removed. It answered positionally from the base CSR while a delta overlay was active, so after deleting an edge the next neighbour inherited the deleted edge's weight - a wrong shortest path, MST or Steiner tree, never an exception. UseedgeWeightsOf()/edgeWeightsForSlice()(#6315).ArcadeGremlin.setTimeout(long, TimeUnit)andgetTimeout()no longer exist. They were a no-op backed by a static field shared across every graph in the process (#5842).ArcadeGraph.close()rolls back an uncommitted transaction instead of committing it, matching TinkerPop's defaultCLOSE_BEHAVIOR.ROLLBACK. A pooled graph is rolled back on release too (#6820, #6821).MutableDocument.getPropertyNames()returns a snapshot, not the live internal key set, soremove()/clear()on it no longer mutates the record behind validation (#6818).- The
arcadedb.sql.parserImplementationsetting is gone with the dead JavaCC-generated legacy parser (~33,000 lines never instantiated at runtime; the ANTLR parser has always been the production one) (#5867). scanType()throws when any bucket scan fails, instead of returning a silently partial scan (#6467).BucketIteratorpropagates a record that fails to load instead of dropping it (#6015).- Deleting a record with a corrupted chunk chain raises
BrokenChunkChainExceptionrather than a retryableConcurrentModificationException(#6282), and a missing vertex reached through an edge list raisesVertexNotFoundException(#6572). - The
propertieskey is omitted fromschema:indexesrows for manual indexes instead of being reported as[[]](#6005).
SQL
- Out-of-range integral writes now fail.
SET n = 3000000000on anINTEGERproperty raises a validation error where it used to store-1294967296. Bulk imports carrying such values will surface the error; use the new-onRowError skipto continue past them (#5905, #5968). sum/avg/min/maxover zero matching rows return one null row instead of an empty result set (#6680).TRUNCATE TYPEinside an explicit transaction is now rolled back byROLLBACKand its index rebuild is deferred to commit (#6220).CREATE PROPERTY ... DEFAULT <unparseable>fails at DDL time instead of storing the literal text on every record (#6134), andRESTORE ... SETvalidates constraints and applies declared defaults (#6127).- A function or method call with the wrong argument count or an incompatible argument type now fails with a validation error and HTTP 400, where it used to throw a raw JDK exception as a 500 or silently coerce (#5884, #5885, and the argument-hardening issues listed above).
- A regex evaluation exceeding
arcadedb.command.regexTimeout(1000 ms, shared by the whole command) fails. A legitimately slowMATCHESpattern needs the setting raised (#5886). - Expressions nested deeper than 200 levels are rejected in both SQL and Cypher; raise
arcadedb.sql.maxExpressionDepth/arcadedb.cypher.maxExpressionDepthif a legitimate query needs more (#5851). - openCypher
MATCHand non-SELECT SQL statements now abort whenarcadedb.command.timeoutelapses, where the deadline used to be honoured only by the SQLSELECTplanner (#6266).
openCypher
- "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, and the Neo4j importer's shared root type is renamed fromNodeto the same sentinel (#6395, #6444). CREATE (n:A|B)andMERGE (n:A|B)raise an error instead of silently inventing anA~Bcomposite type (#6338).- String, numeric and boolean functions reject the wrong type instead of coercing:
toUpper(5)no longer returns"5",sum(['1','2'])no longer returns0,reverse(5)no longer returns null (#5798, #5799, #5801). A malformed direction string such as'INCOMING'is rejected rather than silently treated asBOTH(#6976). AND/ORshort-circuit, so an expression with side effects or errors in the unselected operand is no longer evaluated (#5835).DELETE t ... SET t.v = 99fails and rolls back instead of silently committing the delete (#5795), a subquery referencing an out-of-scope variable raises instead of returning empty (#5825), a statement concluding withCALL ... YIELDand noRETURNis rejected as in Neo4j (#6450), and lexically malformed Cypher that used to parse with the bad token dropped is now rejected (#5958).DISTINCTandUNIONcollapse mixed INTEGER/FLOAT values that compare equal into one row (#5789, #6676).- A whole-vertex projection no longer emits the type's declared properties as top-level null columns; clients reading those keys will not find them (#5613).
- A statement calling a
DEFINE FUNCTIONis classified as a write and routed to the leader on HA (#6418). shortestPath((a)-[*3..5]-(a))returns no row where it used to return the zero-length path;[*]and[*1..]still return it, as in Neo4j (#7017).- A label write on a high-degree node logs a WARNING above
arcadedb.opencypher.labelWriteDegreeWarning(10,000) and can be refused abovearcadedb.opencypher.labelWriteDegreeLimit(off by default). Such a write is O(degree) and changes the vertex's and its edges' RIDs (#6335).
Wire protocols and HTTP
- HTTP requests that relied on
limit: -1or a hugeLIMITto fetch more than 1,000,000 rows in one response now get HTTP 413. Raisearcadedb.server.httpQueryMaxResultRowsor set it to-1(#5719). - HTTP responses no longer contain
@propsunlesstypeHintsis requested, andtoJSON(true)and WebSocket change events no longer carry it (#5812, #5863). - Postgres: a multi-record DML statement in autocommit lands whole or is rolled back whole (
BATCH nstill commits every n records) (#7096). - Postgres:
arcadedb.postgres.queryMaxRows(default 1,000,000,0= unlimited) caps both the simple and the extended protocol, and an over-limit statement is refused rather than risking anOutOfMemoryError(#6679, #7034). - Postgres: BINARY properties are typed as
byteainstead ofvarchar/"char"[], and the OIDs announced for SHORT, BYTE, DATETIME and DECIMAL may differ from previous releases (#6411, #6447). - Redis: a RESP2 null bulk string decodes as
nullinstead of""(#5911). - GraphQL: an unparsable document is classified as non-idempotent (fail-closed) instead of read-only, and selection sets nested deeper than
arcadedb.graphql.maxNestingDepth(200) are rejected (#5853). POST /api/v1/ai/chatdefaults to theapplication/jsonshape its OpenAPI contract documents, rather thantext/event-stream(#6558).- The Gremlin shaded jar's package layout changed for relocated dependencies (#6793).
Operational
- Backups are ~7.5% larger by default, because
arcadedb.backup.compressionLeveldrops from 9 to 1 in exchange for the 27.6x speed-up. Set it back to 9 if archive size matters more than backup time (#6072). - The Docker image no longer pins a 2 GB heap; the JVM sizes it from the container memory limit. Set
ARCADEDB_OPTS_MEMORYexplicitly if you were relying on the fixed value (#6841). arcadedb.txRetryDelayis a cap, not a fixed window, now that retries use exponential backoff with full jitter fromarcadedb.txRetryDelayBase(#5587).- 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).
- A server whose
arcadedb.ha.appendBufferSize/arcadedb.ha.grpcMessageSizeMaxexceeds the 64 MB WAL decode ceiling refuses to start, and a single transaction whose uncompressed WAL exceeds that ceiling is rejected at commit instead of being replicated (#5933). LSM_TREEindexes written before the #5321 comparator change should be rebuilt. The condition is now reported once per logical index as a queryable upgrade warning, visible throughschema:indexesand Studio, naming theREBUILD INDEXto run (#5802).- A clean close writes a
gav-v1.csrfile next to the database for each Graph Analytical View; disable witharcadedb.gavPersistCsr=false(#6583). - A JSONL export or import that skipped records no longer reports success (#6468, #6471).
- Console commands keep their backslashes; they are no longer consumed as shell-style escapes (#6827).
- Reads and writes on a TimeSeries type whose sealed store cannot be loaded raise an error naming the type, instead of silently recreating it empty (#6356).
Dependency updates
Around 120 dependency bumps landed in this cycle, almost all through Dependabot. The notable ones:
- Security-driven: the Gremlin shaded jar is rebuilt on the project-wide Jackson 2.22.2 so it no longer bundles TinkerPop's relocated Jackson 2.15.2 (flagged by Docker Scout and grype),
golang.org/x/netraised to 0.58.0 in the Go E2E harness, and the Studiosecurity-criticalgroup bumped twice, as 6 and then 8 packages. - Runtime and engine: 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, lz4-java 1.11.2, Micrometer 1.17.1, OpenTelemetry 1.65.0, Jedis 8.0.1, Neo4j Java driver 6.2.1, Tomcat JDBC 11.0.25.
- Native image: the GraalVM pin and the native-image builder now move together, and the build is aligned to mainline GraalVM CE, with local build scripts added.
- Build and test: JUnit Jupiter 6.1.3, Cucumber 7.34.7, Maven 3.9.16, native-maven-plugin 1.1.11, docker-maven-plugin 0.49.0, license-maven-plugin 5.1.2, protobuf-maven-plugin 5.1.8, swagger-parser 2.1.47, actions/cache v6 and actions/setup-java v6, plus the usual Playwright, Jest, testcontainers and Go module refreshes.
- Studio: ApexCharts 7.0.0, swagger-ui-dist 5.32.14, marked 18.0.11, cytoscape 3.34.2, plus the routine build-tooling churn.
The Gremlin ANTLR runtime and the Groovy major remain deliberately frozen, as TinkerPop cannot take a newer one.
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, @ZwaarContrast.
Full Changelog: 26.8.1...26.9.1