Skip to content

26.9.1

Latest

Choose a tag to compare

@lvca lvca released this 03 Sep 19:29
· 2 commits to main since this release

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 IN on an indexed property returned the IN result set, DISTINCT ... ORDER BY ... LIMIT returned too few rows, a multi-key GROUP BY grouped on the last key only, and an index range scan could return rows the WHERE clause excluded. All fixed, with regression tests.
  • Indexes are used where they were not - a literal IN (...) list, a composite-index prefix with ORDER 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.compressionThreads and arcadedb.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 /checksums endpoint 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 publishPages blocked 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 SELECT returned a placeholder-backed record under two different RIDs when its content had spilled into chunks, so count(@rid) reported 2 for one record (#6196).
  • A 16% space leak. CRUDTest.multiUpdatesOverlap ended 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 FIX now sweeps them and reports orphanedChunks / 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 with five of eight exhausting TX_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 FIX force-deleted a record with a broken chain, the placeholder pointing at it was left dangling, so count(*) said 8 and a scan said 7, permanently, while the report said totalErrors: 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 IN returned the IN result 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 n returned fewer than n rows, because the Top-K bound was applied before deduplication (#6923).
  • A multi-key GROUP BY grouped on the last key only - the synthetic alias counter was final int i = 0 outside the loop, so every key got the same alias (#6924) - and DISTINCT was silently dropped whenever the statement also had GROUP BY, UNWIND or expand() (#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 WHERE clause (#6927).
  • An indexed range over non-ASCII text returned nothing. BinaryComparator ordered 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_VECTOR index 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 efSearch beam 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 explicit efSearch: 100 is 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()/2 threads with 31.04% of CPU burned in LongAdder.add on the distance path; striped counters cut 2-5x per lookup and the pool defaults to cores minus one, settable with arcadedb.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 WHERE read a missing binding and dropped every row (#6567, #6599, #6600); the same check had no CREATE/MERGE case, so CREATE (c {since: r.since}) wrote null (#6573).
  • Relationship uniqueness was scoped to one pattern part instead of the whole MATCH clause, so the same OPTIONAL MATCH returned different row counts depending on whether the rows went through collect/UNWIND first (#6310).
  • A label write left the row's other aliases pointing at a deleted record, so REMOVE n:l1 after OPTIONAL MATCH failed with RecordNotFoundException (#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 a WHERE never matched and NOT (n: Tag ) passed every row (#6345), and labels() dropped a vertex's own type under inheritance (#6363).
  • MERGE created 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 MATCH with 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 SQL TRAVERSE answered 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:

  • UPDATE on a vertex type in autocommit - the JDBC, psycopg and Spark default - failed with Transaction not active while document and edge updates succeeded. A vertex can now be modified outside a transaction, and UPDATE/DELETE/INSERT in autocommit each run as one statement-level transaction (#7096, open as discussion #1588 since 2024).
  • An error inside BEGIN wedged the session permanently: ReadyForQuery never reported 'E', COMMIT/ROLLBACK were not recognised, and every further statement was silently swallowed (#6457, #6543, #6545, #6548).
  • JDBC fetch size silently truncated results to the first N rows - PortalSuspended was written before the rows and the portal removed, so the follow-up Execute found nothing (#6458).
  • pgjdbc's sixth execution of a PreparedStatement served stale rows: re-Binding an already-executed named statement reused the portal without resetting executed (#6660).
  • Schema probes answered nothing. WHERE 1=0 and LIMIT 0 over a computed projection returned no RowDescription at 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 types ACL 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_database tool bypassed per-type and per-bucket ACL enforcement. Reported by @T4ran24.

Untrusted input reaching the host

  • GHSA-67m7-7w7g-mpmh (high) - the IMPORT DATABASE SSRF 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 DENIED entry for java.util.ResourceBundle was 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_16 element counts bypassed those guards while ListFrame allocated 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 a 3xx redirect or DNS rebinding to an internal address, while import database was already hardened (#6381). The two independent SSRF checks on import database also 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, updateSecurity and updateDatabaseSettings grants and its resultSetLimit/readTimeout were 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 js command from any caller could read a previous caller's parameters and globals (#6759), and the host-class allow-list's ancestor walk skipped package-wildcard DENIED entries (#6045).
  • Credentials stopped being written down. The console wrote every connect remote: ... <password> and create user ... identified by <password> line to ./.history in cleartext and echoed it in -b mode (#6829), and with arcadedb.bolt.debug=true the HELLO message logged the caller's cleartext password (#6801).
  • POST /api/v1/login minted a session per call into an unbounded map, storing untruncated client-controlled headers for at least 30 minutes (#6809), and the db tag of the arcadedb.http.requests meter was the raw path parameter with no existence check, so unauthenticated requests grew the meter registry without bound (#6805).
  • Bolt LOGOFF was 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-shaded ships 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 OutOfMemoryError the recovery guard could not catch (#6932), and PromQL query_range overflowed 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 with CONTENT, SET and INSERT ... FROM <query> (#4918).
  • CHECK DATABASE FIX RECLAIM UNREFERENCED FILES deletes files with no schema component, left behind by an abandoned HA schema instalment sequence, and reports them (#6189).
  • CHECK DATABASE ... DEEP is a new tier for the expensive TimeSeries sealed-store checks, with a FIX arm 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, decode and strcmpci do (#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.mergeNodes and apoc.refactor.cloneNodesWithRelationships (#6059, #6060, #6157).
  • db.index.fulltext.queryNodes / db.index.fulltext.queryRelationships YIELD (node|relationship, score) from ArcadeDB's BM25 FULL_TEXT index inside a Cypher statement, matching Neo4j (#6729).
  • Cypher dynamic labels SET n:$(expr) and REMOVE n:$(expr), plus REMOVE n IS Label (#7059, #7093).
  • The GQL standalone FILTER clause 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 a grpc: field in arcadedb.ha.serverList, so a follower refusing graphBatchLoad can name a dialable gRPC address in the arcadedb-leader-grpc-address trailer instead of only the leader's HTTP address (#6091).
  • /api/v1/cluster reports live Raft membership. Every peer carries inConfiguration, a declared peer the cluster no longer contains reports role NOT_IN_CONFIGURATION, the divergence raises peers-not-in-configuration / peers-not-in-server-list alerts, and Studio shows the state (#7040).
  • A skip mode for the bulk importer. -onRowError skip|abort (default abort) 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.txRetryDelayBase and doubling per attempt up to the arcadedb.txRetryDelay cap, 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_ms and arcadedb.ha.schema.unreferenced_files per database (#6143, #6144), page-snapshot metrics (#6116), and vector-index getUpgradeWarning() / untrustedSegments / deferred-rebuild state (#6566, #6657).

Major fixes and improvements

Storage and integrity

  • CHECK DATABASE at scale. A hub vertex's adjacency list was re-walked once per edge, O(degree²) on super-nodes: one CHECK DATABASE FIX on 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 GraphBatch import and a DELETE ... BATCH loop. 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 while linkSuperType() had contributed the whole subtree, so after unlinking B from A a grandchild's records still came back from SELECT FROM A (#6935).
  • A misaligned read no longer answers with invented property names pulled from the dictionary (#5774), reload() on an ImmutableVertex re-parses its edge pointers (#5771), and an AfterRecordReadListener returning a modified record with an EXTERNAL property 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/EEE rendered month names in the JVM default locale, so a schema date written on an it_IT node failed to parse on another, and FileUtils.copyFile ignored transferTo's return value so a file over 2 GB was silently truncated (#7112).

Indexes

  • CREATE INDEX <name> IF NOT EXISTS answered created: true under the requested name while silently reusing a pre-existing index on the same property, so SEARCH_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 recreated FULL_TEXT, geospatial and LSM_SPARSE_VECTOR indexes from the underlying LSM-Tree's metadata, so analyzers, BM25 parameters, geohash resolution and sparse-vector settings silently reverted to defaults (#5742, #5934); REBUILD INDEX dropped a named index's logical name (#5791); and JSONL import rebuilt them without their metadata (#5650).
  • REBUILD INDEX no longer returns silently when it fails. Both it and CHECK DATABASE FIX retried 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 indexed INTEGER column) threw ClassCastException while the un-indexed equivalent worked (#5932).
  • CONTAINSTEXT on a single-property full-text index split its literal on :, so any value containing a colon returned no matches (#6382); two CONTAINSTEXT conditions 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_TREE index stores a LINK key 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 INDEX leaving 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 antisymmetry BinaryComparator needs 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/BYTE property wrapped it: SET n = 3000000000 stored -1294967296 with no error (#5905).
  • SUM()/AVG() over an INTEGER column overflowed silently once the running sum passed Integer.MAX_VALUE: five rows of 2,000,000,000 gave sum = 5705032704 instead of 10,000,000,000 (#5906).
  • LIMIT 2147483648 narrowed to Integer.MIN_VALUE and returned 0 rows, a finite double above Float.MAX_VALUE was dropped from map JSON, and a DOUBLE MIN/MAX constraint was checked as float (#5919).
  • BinaryComparator narrowed the wider operand to the first operand's width, giving a non-antisymmetric order, and parsed string operands with Integer.parseInt, so WHERE n < 'abc' crashed (#5900). A STRING compared against a DATE/DATETIME was compared lexicographically (#5947, #5956).
  • NaN narrowed to 0 when converting a Double/Float to 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 a String[], and the operator surface now handles it. CONTAINS on either side, CONTAINSANY, and join()/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/max over zero matching rows returned an empty result set while count(*) 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* and dijkstra() 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.timeout now bounds what it claims to. The deadline belongs to the CommandContext (inherited by subqueries, UNION branches and parallel scan workers), and is checked inside openCypher scans, expansions and joins, SQL TRAVERSE/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=0 and LIMIT 0 fold to an EMPTY RESULT step at plan time instead of scanning the target, and WHERE 1=1 folds away instead of being evaluated per record (#6174, #6184).
  • SELECT FROM $var mutated 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 concurrent DROP INDEX cannot be stored and reused (#6671).
  • Result.toJSON() rendered every embedded document in a projection as null (#6945), and MatchStatement.toString() never rendered NOT {...} patterns, so a materialized view round-tripped through its text silently lost its negative filter (#6999).
  • A property DEFAULT that 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 PROPERTY on a property with a DEFAULT left its name in the type's cache, so every later insert failed with SchemaException: Cannot find property (#6799).
  • The native select() builder applied the result limit to the index candidate scan, so IS NOT NULL with paging returned 101 of 500 rows (#6565); treated OR with a neq/like/ilike leaf as fully indexed, returning 0 rows instead of 1,000 (#6577); dropped timeout() 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 VarLengthExpand operator 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-pattern MATCH sharing two labelled variables returned 0 rows (#6322); an unlabelled anchor could not be counted at all (#5757); a correlated COUNT { } body lost the push-down and materialised one row per edge (#5758); and every CountOp sized 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 MATCH after a relationship pattern bound null instead of producing the Cartesian product (#5810), and EXISTS { } silently accepted a variable a preceding WITH had dropped from scope (#5825).
  • DISTINCT and UNION treated 1 and 1.0 as distinct although 1 = 1.0 is true (#5789, #6676), built their dedup key by string-concatenating name=value| so values containing = or | could collapse (#6540), and included unused path and anonymous pattern variables in the key (#6488, #6541).
  • shortestPath() and allShortestPaths() ignored the pattern's hop bounds, so shortestPath((s)-[:R*..2]-(e)) returned a 4-hop path (#7009, #7017).
  • AND/OR did not short-circuit, so false AND E surfaced E'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 SET used a private re-implementation of SET that handled only variable.property, silently dropping dynamic keys and expression targets, and SET n = m with 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 ignored Thread.interrupt(), arcadedb.command.timeout and client cancellation (#6302, #6295, #6318); the graph an algo.* call loads, the embedding matrices and the nodeCount² bitsets are all priced against arcadedb.cypher.algoMaxWorkingMemory now (#6317, #6263, #6300, #6375); and algo.apsp streams its up to n²-n rows instead of materialising them (#6296).
  • Two algo.* wrong answers: algo.steinerTree and algo.maxKCut paired edge weights with neighbours by iteration position, so a relTypes filter or the mere presence of a Graph Analytical View produced wrong trees, weights and partitions (totalWeight 1000.0 for a tree costing 2.0) (#6301, #6376). algo.wcc ignored its relTypes argument (#6699) and algo.degree ignored its direction (#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 every algo.* 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.supernodeInterleaveRounds degrading 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.moveEdge deleted 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 <> c inequality 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).
  • GraphBatch retained 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 per GraphBatch flush - force-exiting every other user's queued tasks (2,183 InterruptedIOExceptions in one production log); the flags are plain volatile writes now, and setParallelLevel() resizes in place (#6509, #5665, #6526).
  • Async writes now behave like synchronous ones: updateRecord() never called validate() (#7002), deleteRecord() fired every before- and after-delete listener twice (#7003), onOk fired 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 LONG on 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, lookupByKey never 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/users mutated the user store only on the node that served the request, while the equivalent create user command replicated through Raft, so a user created via REST got 401 on the other nodes (#6808).
  • RaftGroupCommitter awaited 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); the Quorum.ALL watch loop had the same shape (#6373).
  • A follower whose log writer hit No space left on device stayed 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); localhost and 127.0.0.1 were not recognised as the same endpoint (#6204); verify could fan out to itself and report ALL_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 FIX works 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, PostVerifyDatabaseHandler and HAReplicationMetrics (#5890, #5850), and a Kubernetes auto-join probe with a retryForeverNoSleep policy 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: or returned NaN whenever both sides shared a label set, label matchers on an absent column matched backwards, range points were not step-aligned (#6938), and min_over_time/max_over_time returned ±Infinity or Double.MAX_VALUE for 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/deleteOne by ObjectId _id never matched (#6745); skip and sort were silently ignored (#6746, #6747); $exists: false returned the documents that had the field, and $not emitted invalid SQL (#6748); one document with a non-hex _id made every subsequent read of the collection throw (#6939); an upsert filtered on _id discarded it and created a duplicate on every call (#6940); insert reported n + 1 and count ignored its query (#6941); and {field: null} matched nothing (#6952).
  • Bolt. A second RUN inside 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 declared ARRAY_OF_FLOATS read back as [F@294b13ce instead 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); and CALL merge.relationship(...) failed with "Transaction not active" while the same Cypher worked over HTTP (#6547).
  • gRPC. insertStream and bulkInsert ignored the caller's TransactionContext and committed on their own, so rows survived a subsequent rollback (#6607); lookupByRID() threw RecordNotFoundException for a vertex a SQL query over the same connection found (#6404); errors were flattened to success=false so the client lost the exception type and never retried a conflict (#6192); a stream longer than txMaxIdleMs was reaped mid-stream and its rows lost (#6755); batch() silently sent JSONL over HTTP instead of the streaming RPC (#6070); and StreamQuery in PAGED mode discarded the caller's own ORDER 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 standard query($a: String, $b: Int) failed to parse because the comma was a real token (#6860); Boolean argument values were always null and Float values 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); SET ignored all its options and INCRBY parsed a 32-bit amount (#6466); and error replies were always prefixed -ERR even for WRONGPASS/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). A RemoteDatabase-backed traversal leaked a Netty event-loop group per graph instance (#6822), the arcadedb-gremlin coordinate 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. httpQueryDefaultLimit protected only callers that stated no limit, so LIMIT 100000000 or "limit": -1 made the server serialise an unbounded result into one JSON response; the new arcadedb.server.httpQueryMaxResultRows (default 1,000,000) refuses with HTTP 413 rather than truncating (#5719).
  • A retryable conflict on POST /api/v1/command answered 500 because DatabaseAbstractHandler wrapped every handler exception in a TransactionException (#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), and RemoteGraphBatch.flush() left the payload buffered on failure so close() re-sent it and duplicated committed records (#7031).
  • The @props type hint leaked into every response. It appeared in HTTP JSON results for non-element rows, in toJSON(true) and in WebSocket change events broadcast to every subscriber; it is opt-in now through a typeHints request 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 with load (#6827); an unterminated { swallowed every following ;-separated command (#6392, #6439); connect remote: failed on a password containing a space (#6830); and close() could lose buffered output (#6828).
  • Kubernetes and Docker quickstarts that could not work. The StatefulSet example used ${VAR} in command:, 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, so docker run -m 512m died at startup (#6841), and the README and compose quickstarts passed settings via JAVA_OPTS, silently replacing the image's ZGC flags with G1 (#6842).
  • MCP: full_text_search had no upper bound on limit, set_server_setting accepted 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. Use edgeWeightsOf() / edgeWeightsForSlice() (#6315).
  • ArcadeGremlin.setTimeout(long, TimeUnit) and getTimeout() 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 default CLOSE_BEHAVIOR.ROLLBACK. A pooled graph is rolled back on release too (#6820, #6821).
  • MutableDocument.getPropertyNames() returns a snapshot, not the live internal key set, so remove()/clear() on it no longer mutates the record behind validation (#6818).
  • The arcadedb.sql.parserImplementation setting 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). BucketIterator propagates a record that fails to load instead of dropping it (#6015).
  • Deleting a record with a corrupted chunk chain raises BrokenChunkChainException rather than a retryable ConcurrentModificationException (#6282), and a missing vertex reached through an edge list raises VertexNotFoundException (#6572).
  • The properties key is omitted from schema:indexes rows for manual indexes instead of being reported as [[]] (#6005).

SQL

  • Out-of-range integral writes now fail. SET n = 3000000000 on an INTEGER property raises a validation error where it used to store -1294967296. Bulk imports carrying such values will surface the error; use the new -onRowError skip to continue past them (#5905, #5968).
  • sum/avg/min/max over zero matching rows return one null row instead of an empty result set (#6680).
  • TRUNCATE TYPE inside an explicit transaction is now rolled back by ROLLBACK and 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), and RESTORE ... SET validates 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 slow MATCHES pattern needs the setting raised (#5886).
  • Expressions nested deeper than 200 levels are rejected in both SQL and Cypher; raise arcadedb.sql.maxExpressionDepth / arcadedb.cypher.maxExpressionDepth if a legitimate query needs more (#5851).
  • openCypher MATCH and non-SELECT SQL statements now abort when arcadedb.command.timeout elapses, where the deadline used to be honoured only by the SQL SELECT planner (#6266).

openCypher

  • "No labels" is a reserved sentinel type ~NO_LABEL~. V and Vertex are ordinary labels now, so labels(n) on a vertex whose only label was V changes on pre-26.9.1 data, and the Neo4j importer's shared root type is renamed from Node to the same sentinel (#6395, #6444).
  • CREATE (n:A|B) and MERGE (n:A|B) raise an error instead of silently inventing an A~B composite 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 returns 0, reverse(5) no longer returns null (#5798, #5799, #5801). A malformed direction string such as 'INCOMING' is rejected rather than silently treated as BOTH (#6976).
  • AND/OR short-circuit, so an expression with side effects or errors in the unselected operand is no longer evaluated (#5835).
  • DELETE t ... SET t.v = 99 fails 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 with CALL ... YIELD and no RETURN is rejected as in Neo4j (#6450), and lexically malformed Cypher that used to parse with the bad token dropped is now rejected (#5958).
  • DISTINCT and UNION collapse 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 FUNCTION is 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 above arcadedb.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: -1 or a huge LIMIT to fetch more than 1,000,000 rows in one response now get HTTP 413. Raise arcadedb.server.httpQueryMaxResultRows or set it to -1 (#5719).
  • HTTP responses no longer contain @props unless typeHints is requested, and toJSON(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 n still 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 an OutOfMemoryError (#6679, #7034).
  • Postgres: BINARY properties are typed as bytea instead of varchar/"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 null instead 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/chat defaults to the application/json shape its OpenAPI contract documents, rather than text/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.compressionLevel drops 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_MEMORY explicitly if you were relying on the fixed value (#6841).
  • arcadedb.txRetryDelay is a cap, not a fixed window, now that retries use exponential backoff with full jitter from arcadedb.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.grpcMessageSizeMax exceeds 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_TREE indexes written before the #5321 comparator change should be rebuilt. The condition is now reported once per logical index as a queryable upgrade warning, visible through schema:indexes and Studio, naming the REBUILD INDEX to run (#5802).
  • A clean close writes a gav-v1.csr file next to the database for each Graph Analytical View; disable with arcadedb.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/net raised to 0.58.0 in the Go E2E harness, and the Studio security-critical group 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