<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://arcadedb.com/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://arcadedb.com/" rel="alternate" type="text/html" /><updated>2026-09-10T16:19:14+00:00</updated><id>https://arcadedb.com/blog/feed.xml</id><title type="html">ArcadeDB</title><subtitle>The Next Generation Multi-Model Database</subtitle><entry><title type="html">Native ArcadeDB Drivers for Python and TypeScript, Over HTTP and gRPC</title><link href="https://arcadedb.com/blog/arcadedb-native-drivers-python-typescript/" rel="alternate" type="text/html" title="Native ArcadeDB Drivers for Python and TypeScript, Over HTTP and gRPC" /><published>2026-09-09T00:00:00+00:00</published><updated>2026-09-09T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-native-drivers-python-typescript</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-native-drivers-python-typescript/"><![CDATA[<p>Until now, talking to ArcadeDB from Python or Node meant one of two things: writing HTTP calls by hand against the REST API, or borrowing a driver built for another database and living inside the subset of ArcadeDB that other database’s protocol happens to expose. Both work. Neither is a client anyone would call native.</p>

<p>That changes with <a href="https://github.com/ArcadeData/arcadedb-drivers">arcadedb-drivers</a>, a new repository holding four published clients: an HTTP driver and a gRPC driver for Python, and an HTTP driver and a gRPC driver for TypeScript/JavaScript. All four are Apache-2.0, all four are generated from contracts ArcadeDB itself publishes, and all four are on the public registries today. They are also young: 0.1.0 releases under heavy development, with more languages on the way.</p>

<h2 id="the-four-packages">The four packages</h2>

<table>
  <thead>
    <tr>
      <th>Package</th>
      <th>Language</th>
      <th>API</th>
      <th>Install</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://pypi.org/project/arcadedb-driver/"><code class="language-plaintext highlighter-rouge">arcadedb-driver</code></a></td>
      <td>Python</td>
      <td>HTTP</td>
      <td><code class="language-plaintext highlighter-rouge">pip install arcadedb-driver</code></td>
    </tr>
    <tr>
      <td><a href="https://pypi.org/project/arcadedb-driver-grpc/"><code class="language-plaintext highlighter-rouge">arcadedb-driver-grpc</code></a></td>
      <td>Python</td>
      <td>gRPC</td>
      <td><code class="language-plaintext highlighter-rouge">pip install arcadedb-driver-grpc</code></td>
    </tr>
    <tr>
      <td><a href="https://www.npmjs.com/package/@arcadedb/driver"><code class="language-plaintext highlighter-rouge">@arcadedb/driver</code></a></td>
      <td>TypeScript/JS</td>
      <td>HTTP</td>
      <td><code class="language-plaintext highlighter-rouge">npm install @arcadedb/driver</code></td>
    </tr>
    <tr>
      <td><a href="https://www.npmjs.com/package/@arcadedb/driver-grpc"><code class="language-plaintext highlighter-rouge">@arcadedb/driver-grpc</code></a></td>
      <td>TypeScript/JS</td>
      <td>gRPC</td>
      <td><code class="language-plaintext highlighter-rouge">npm install @arcadedb/driver-grpc</code></td>
    </tr>
  </tbody>
</table>

<p>All four talk to a running server, so they assume the <a href="/client-server.html">client-server</a> deployment rather than the embedded one. The Python packages need Python 3.10 or newer. The TypeScript packages need Node 20 or newer and are ESM only: import them, do not <code class="language-plaintext highlighter-rouge">require()</code> them. Version 0.1.0 of each targets ArcadeDB server 26.9.1, and every package README carries a compatibility table mapping driver versions to server versions.</p>

<h2 id="http-or-grpc">HTTP or gRPC?</h2>

<p>Two drivers per language is not indecision. HTTP and gRPC are the two transports ArcadeDB speaks, and each handles some workloads better than the other.</p>

<table>
  <thead>
    <tr>
      <th>Use the HTTP driver when</th>
      <th>Use the gRPC driver when</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>You are writing browser code</td>
      <td>You are writing server-to-server code</td>
    </tr>
    <tr>
      <td>You want the fewest dependencies</td>
      <td>You are reading a large result set and want to stream it</td>
    </tr>
    <tr>
      <td>You are in a serverless function with cold starts to worry about</td>
      <td>You are bulk-inserting and want one long-lived connection</td>
    </tr>
    <tr>
      <td>Your traffic is ordinary request/response</td>
      <td>Your traffic is throughput-sensitive and sustained</td>
    </tr>
    <tr>
      <td>You want to reuse existing HTTP infrastructure: proxies, gateways, tracing</td>
      <td>You want native bidirectional streaming</td>
    </tr>
  </tbody>
</table>

<p>Start with HTTP. It works everywhere, it needs nothing beyond <code class="language-plaintext highlighter-rouge">fetch</code> or <code class="language-plaintext highlighter-rouge">httpx</code>, and for most application traffic the protocol is not the bottleneck. Move a workload to gRPC when you can point at the throughput number that justifies it.</p>

<p>One constraint is easy to lose an afternoon to, so here it is plainly. <strong>There is no browser build of the gRPC driver, and there will not be one until the server changes.</strong> ArcadeDB’s <code class="language-plaintext highlighter-rouge">GrpcServerPlugin</code> is plain grpc-java over HTTP/2, built on Netty, with no gRPC-Web handler, no Connect protocol, and no servlet adapter in front of it. A browser cannot speak raw HTTP/2 gRPC framing, so no client library in any language can reach this server from a browser tab. Browser code uses the HTTP driver. The limit is in the server.</p>

<h2 id="connecting-and-querying">Connecting and querying</h2>

<p>The HTTP drivers are the place to start. Python, synchronously:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">arcadedb_driver</span> <span class="kn">import</span> <span class="n">ArcadeDBServer</span><span class="p">,</span> <span class="n">basic_auth</span>

<span class="k">with</span> <span class="nc">ArcadeDBServer</span><span class="p">(</span><span class="n">base_url</span><span class="o">=</span><span class="sh">"</span><span class="s">http://localhost:2480</span><span class="sh">"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="nf">basic_auth</span><span class="p">(</span><span class="sh">"</span><span class="s">root</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">playwithdata</span><span class="sh">"</span><span class="p">))</span> <span class="k">as</span> <span class="n">srv</span><span class="p">:</span>
    <span class="n">db</span> <span class="o">=</span> <span class="n">srv</span><span class="p">.</span><span class="nf">db</span><span class="p">(</span><span class="sh">"</span><span class="s">mydb</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">envelope</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="n">command</span><span class="o">=</span><span class="sh">"</span><span class="s">SELECT FROM Person WHERE age &gt; ?</span><span class="sh">"</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">1</span><span class="sh">"</span><span class="p">:</span> <span class="mi">21</span><span class="p">})</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">envelope</span><span class="p">.</span><span class="n">result</span><span class="p">)</span>
</code></pre></div></div>

<p>The async facade mirrors the sync one method for method:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">asyncio</span>
<span class="kn">from</span> <span class="n">arcadedb_driver</span> <span class="kn">import</span> <span class="n">AsyncArcadeDBServer</span><span class="p">,</span> <span class="n">basic_auth</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="k">async</span> <span class="k">with</span> <span class="nc">AsyncArcadeDBServer</span><span class="p">(</span><span class="n">base_url</span><span class="o">=</span><span class="sh">"</span><span class="s">http://localhost:2480</span><span class="sh">"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="nf">basic_auth</span><span class="p">(</span><span class="sh">"</span><span class="s">root</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">playwithdata</span><span class="sh">"</span><span class="p">))</span> <span class="k">as</span> <span class="n">srv</span><span class="p">:</span>
        <span class="n">db</span> <span class="o">=</span> <span class="n">srv</span><span class="p">.</span><span class="nf">db</span><span class="p">(</span><span class="sh">"</span><span class="s">mydb</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">envelope</span> <span class="o">=</span> <span class="k">await</span> <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="n">command</span><span class="o">=</span><span class="sh">"</span><span class="s">SELECT FROM Person WHERE age &gt; ?</span><span class="sh">"</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">1</span><span class="sh">"</span><span class="p">:</span> <span class="mi">21</span><span class="p">})</span>
        <span class="nf">print</span><span class="p">(</span><span class="n">envelope</span><span class="p">.</span><span class="n">result</span><span class="p">)</span>

<span class="n">asyncio</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="nf">main</span><span class="p">())</span>
</code></pre></div></div>

<p>Both are context managers because both own an <code class="language-plaintext highlighter-rouge">httpx</code> client with its own connection pool that has to be released. One detail that surprises people in production: omitting <code class="language-plaintext highlighter-rouge">timeout</code> disables timeouts entirely instead of falling back to httpx’s five-second default, because in httpx an explicit <code class="language-plaintext highlighter-rouge">timeout=None</code> means exactly that. Pass an <code class="language-plaintext highlighter-rouge">httpx.Timeout</code> if you want requests bounded.</p>

<p>TypeScript, same query:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">createClient</span><span class="p">,</span> <span class="nx">basicAuth</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@arcadedb/driver</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nf">createClient</span><span class="p">({</span>
  <span class="na">baseUrl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:2480</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">auth</span><span class="p">:</span> <span class="nf">basicAuth</span><span class="p">(</span><span class="dl">"</span><span class="s2">root</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">playwithdata</span><span class="dl">"</span><span class="p">),</span>
<span class="p">});</span>

<span class="kd">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="nx">server</span><span class="p">.</span><span class="nf">db</span><span class="p">(</span><span class="dl">"</span><span class="s2">mydb</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">result</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nf">query</span><span class="p">({</span>
  <span class="na">language</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sql</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">command</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SELECT FROM Person WHERE age &gt; ?</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="mi">1</span><span class="p">:</span> <span class="mi">21</span> <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<p>A bearer token, such as a session token returned by <code class="language-plaintext highlighter-rouge">/api/v1/login</code>, works the same way in both languages: swap <code class="language-plaintext highlighter-rouge">basic_auth</code> for <code class="language-plaintext highlighter-rouge">bearer_auth</code>, or <code class="language-plaintext highlighter-rouge">basicAuth</code> for <code class="language-plaintext highlighter-rouge">bearerAuth</code>.</p>

<p>Because ArcadeDB is multi-model, <code class="language-plaintext highlighter-rouge">language</code> does real work here. <code class="language-plaintext highlighter-rouge">"sql"</code>, <code class="language-plaintext highlighter-rouge">"cypher"</code>, <code class="language-plaintext highlighter-rouge">"gremlin"</code>: the same <code class="language-plaintext highlighter-rouge">query</code> call reaches all of them, and the driver does not care which one you picked.</p>

<h2 id="the-result-envelope-and-why-truncated-matters">The result envelope, and why <code class="language-plaintext highlighter-rouge">truncated</code> matters</h2>

<p>Neither HTTP driver returns a bare array of rows. Both return the whole response envelope:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">QueryEnvelope</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="na">result</span><span class="p">:</span> <span class="nx">T</span><span class="p">[];</span>
  <span class="nl">limit</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">returned</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">truncated</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Of every API decision in the drivers, this is the one most likely to look like pointless ceremony, so here is the case for it. <code class="language-plaintext highlighter-rouge">truncated</code> is <code class="language-plaintext highlighter-rouge">true</code> when the server’s serializer hit its row cap while the query still had rows left to write. When that happens, <code class="language-plaintext highlighter-rouge">result</code> is a partial answer, not a short but complete one, and the two are indistinguishable by shape: a 5,000-row array that stopped early looks exactly like a 5,000-row array that ran out of matching records. A driver that unwrapped the envelope and returned only <code class="language-plaintext highlighter-rouge">result</code> would be handing you a value you cannot check.</p>

<p>So check it. When <code class="language-plaintext highlighter-rouge">truncated</code> is <code class="language-plaintext highlighter-rouge">true</code>, re-query with a narrower filter or a higher <code class="language-plaintext highlighter-rouge">limit</code>. Raising <code class="language-plaintext highlighter-rouge">limit</code> is not always the fix, though: a result whose true size exceeds the server’s hard ceiling (<code class="language-plaintext highlighter-rouge">arcadedb.server.httpQueryMaxResultRows</code>) is refused outright with a 413 rather than truncated, and past that point a narrower filter is the only way forward.</p>

<h2 id="transactions">Transactions</h2>

<p>Both HTTP drivers wrap ArcadeDB’s server-side transaction sessions in the idiom their language already has. Python uses a context manager:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">srv</span><span class="p">.</span><span class="nf">db</span><span class="p">(</span><span class="sh">"</span><span class="s">mydb</span><span class="sh">"</span><span class="p">).</span><span class="nf">transaction</span><span class="p">()</span> <span class="k">as</span> <span class="n">tx</span><span class="p">:</span>
    <span class="n">tx</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="n">command</span><span class="o">=</span><span class="sh">"</span><span class="s">INSERT INTO Account SET balance = 100</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">total</span> <span class="o">=</span> <span class="n">tx</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="n">language</span><span class="o">=</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="n">command</span><span class="o">=</span><span class="sh">"</span><span class="s">SELECT sum(balance) as total FROM Account</span><span class="sh">"</span><span class="p">).</span><span class="n">result</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="sh">"</span><span class="s">total</span><span class="sh">"</span><span class="p">]</span>
</code></pre></div></div>

<p>TypeScript uses a callback:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">total</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nf">transaction</span><span class="p">(</span><span class="k">async </span><span class="p">(</span><span class="nx">tx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">command</span><span class="p">({</span> <span class="na">language</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sql</span><span class="dl">"</span><span class="p">,</span> <span class="na">command</span><span class="p">:</span> <span class="dl">"</span><span class="s2">INSERT INTO Account SET balance = 100</span><span class="dl">"</span> <span class="p">});</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">result</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">query</span><span class="p">({</span> <span class="na">language</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sql</span><span class="dl">"</span><span class="p">,</span> <span class="na">command</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SELECT sum(balance) as total FROM Account</span><span class="dl">"</span> <span class="p">});</span>
  <span class="k">return</span> <span class="nx">result</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">total</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The rule in both is the same, and it is the one to get right: <strong>every call that should take part in the transaction goes through the <code class="language-plaintext highlighter-rouge">tx</code> handle, not the outer <code class="language-plaintext highlighter-rouge">db</code> object you opened it from.</strong> A call made through the outer handle while a transaction is open auto-commits on its own, outside the transaction, exactly as if no transaction were open.</p>

<p>The commit and rollback contract has three clauses in both languages. The block exits cleanly and the transaction commits. The block raises and the transaction rolls back, with the block’s own exception propagating: if the rollback also fails, that failure is attached as <code class="language-plaintext highlighter-rouge">__cause__</code> (Python) or <code class="language-plaintext highlighter-rouge">err.cause</code> (TypeScript) rather than replacing the error you actually asked about. And if the commit itself fails, a best-effort rollback is issued first, so the server-side session is not left open until <code class="language-plaintext highlighter-rouge">arcadedb.server.httpTxExpireTimeout</code> reaps it, before the commit’s error is re-raised.</p>

<h2 id="streaming-over-grpc">Streaming, over gRPC</h2>

<p>The gRPC drivers exist for the workloads where HTTP’s request/response shape is the cost. A large result set over HTTP means paging through repeated calls; over gRPC it is one stream.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">arcadedb_driver_grpc</span> <span class="kn">import</span> <span class="n">create_client</span><span class="p">,</span> <span class="n">messages</span>

<span class="k">with</span> <span class="nf">create_client</span><span class="p">(</span><span class="sh">"</span><span class="s">localhost:50051</span><span class="sh">"</span><span class="p">,</span> <span class="n">insecure</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">raw</span><span class="p">.</span><span class="nc">ExecuteQuery</span><span class="p">(</span>
        <span class="n">messages</span><span class="p">.</span><span class="nc">ExecuteQueryRequest</span><span class="p">(</span><span class="n">database</span><span class="o">=</span><span class="sh">"</span><span class="s">mydb</span><span class="sh">"</span><span class="p">,</span> <span class="n">query</span><span class="o">=</span><span class="sh">"</span><span class="s">SELECT FROM Person WHERE age &gt; 21</span><span class="sh">"</span><span class="p">,</span> <span class="n">language</span><span class="o">=</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">)</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>Note the <code class="language-plaintext highlighter-rouge">target</code>: gRPC’s native <code class="language-plaintext highlighter-rouge">host:port</code> form, not a URL. There is no scheme to parse and nothing to default, so you pass <code class="language-plaintext highlighter-rouge">credentials=grpc.ssl_channel_credentials()</code> for TLS or say <code class="language-plaintext highlighter-rouge">insecure=True</code> explicitly.</p>

<p><code class="language-plaintext highlighter-rouge">raw</code> is the generated stub for the whole <code class="language-plaintext highlighter-rouge">ArcadeDbService</code>, so every RPC in the contract is reachable through it. On top, each client adds three wrappers for the RPCs the generated stub alone handles badly: <code class="language-plaintext highlighter-rouge">stream_query</code>, <code class="language-plaintext highlighter-rouge">insert_stream</code>, and <code class="language-plaintext highlighter-rouge">transaction</code>. Streaming a query in TypeScript:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">row</span> <span class="k">of</span> <span class="nx">grpc</span><span class="p">.</span><span class="nf">streamQuery</span><span class="p">({</span>
  <span class="na">database</span><span class="p">:</span> <span class="dl">"</span><span class="s2">mydb</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">SELECT FROM Person</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">language</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sql</span><span class="dl">"</span><span class="p">,</span>
<span class="p">}))</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">row</span><span class="p">.</span><span class="nx">rid</span><span class="p">,</span> <span class="nx">row</span><span class="p">.</span><span class="nx">properties</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">streamQuery</code> flattens the server’s stream of row batches into one row at a time, and that is the only thing it does. It deliberately does not pick <code class="language-plaintext highlighter-rouge">retrievalMode</code> for you, because the three modes differ in ways only the caller can weigh:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">CURSOR</code>, the default: runs the query once and streams results as you iterate. Pick it for a large result set you want to bound memory on.</li>
  <li><code class="language-plaintext highlighter-rouge">MATERIALIZE_ALL</code>: loads the whole result set on the server first, then emits it in batches. Pick it when you need a stable snapshot and can afford to hold it server-side.</li>
  <li><code class="language-plaintext highlighter-rouge">PAGED</code>: re-issues the query with <code class="language-plaintext highlighter-rouge">LIMIT</code>/<code class="language-plaintext highlighter-rouge">SKIP</code> per batch. Pick it when you want each batch’s consistency independent of the others.</li>
</ul>

<p>Streaming inserts work the other way round. You hand <code class="language-plaintext highlighter-rouge">insert_stream</code> an async iterable of row batches, decide yourself how many rows go in a batch, and the driver owns the envelope bookkeeping that is easy to get wrong by hand: one stable session id for the whole stream, <code class="language-plaintext highlighter-rouge">chunk_seq</code> starting at 1 and incrementing, <code class="language-plaintext highlighter-rouge">database</code> set on the first chunk only, and <code class="language-plaintext highlighter-rouge">last: true</code> on the final one.</p>

<p>One security rule the gRPC drivers enforce in code instead of documenting and hoping. <code class="language-plaintext highlighter-rouge">password_auth</code> sends the password in plaintext gRPC metadata, so <code class="language-plaintext highlighter-rouge">create_client</code> refuses to pair it with a channel that has no transport credentials unless you pass <code class="language-plaintext highlighter-rouge">insecure=True</code> and say you meant it:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">create_client</span><span class="p">(</span><span class="sh">"</span><span class="s">localhost:50051</span><span class="sh">"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="nf">password_auth</span><span class="p">(</span><span class="sh">"</span><span class="s">root</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">playwithdata</span><span class="sh">"</span><span class="p">))</span>
<span class="c1"># raises InsecureChannelError
</span>
<span class="nf">create_client</span><span class="p">(</span><span class="sh">"</span><span class="s">localhost:50051</span><span class="sh">"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="nf">password_auth</span><span class="p">(</span><span class="sh">"</span><span class="s">root</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">playwithdata</span><span class="sh">"</span><span class="p">),</span> <span class="n">credentials</span><span class="o">=</span><span class="n">grpc</span><span class="p">.</span><span class="nf">ssl_channel_credentials</span><span class="p">())</span>
<span class="c1"># fine, the channel is encrypted
</span></code></pre></div></div>

<p>A bearer token is not a password and never trips this guard.</p>

<h2 id="one-contract-many-clients">One contract, many clients</h2>

<p>In a year, the four packages will matter less than where their code comes from.</p>

<p><code class="language-plaintext highlighter-rouge">contracts/</code> in the repository holds two files: the OpenAPI specification every HTTP client is generated from, and the Protobuf <code class="language-plaintext highlighter-rouge">.proto</code> every gRPC client is generated from. Both are fetched from ArcadeDB itself; neither is maintained by hand alongside it. No client package edits its generated types. Each client’s build regenerates from the contract and fails on a <strong>drift gate</strong>: if the checked-in generated code and a fresh regeneration disagree, the build breaks rather than shipping a client that quietly describes a server that no longer exists.</p>

<p>This is the failure mode the design is aimed at. Hand-written drivers rot silently. The server adds a field or tightens a response, and the driver keeps compiling and keeps returning plausible values until someone loses a day to it. A generated client with a drift gate cannot get there: the disagreement becomes a red build the day the contract moves, and adding a language later means writing a generator config, not re-reading the server’s source and hand-writing types.</p>

<p>Two smaller things follow from the same idea. Every release is built and published by CI from a clean checkout, never from anyone’s laptop: the npm packages carry provenance attestations, and the PyPI packages go out through trusted publishing with no long-lived token anywhere in the chain. And nothing publishes automatically. Every release is a human-triggered workflow dispatch.</p>

<h2 id="what-is-next">What is next</h2>

<p>These drivers are under heavy development. They are 0.1.0 releases, and while the APIs above are the ones we intend to keep, some of them will change before 1.0. Pin a version if you need the surface to hold still, and read the changelog before you move off it.</p>

<p>More languages are planned. The repository is already laid out for them: <code class="language-plaintext highlighter-rouge">go/</code> and other language directories will appear as siblings of <code class="language-plaintext highlighter-rouge">typescript/</code> and <code class="language-plaintext highlighter-rouge">python/</code>, each generated from the same two contracts. None exist yet, but the contract-first design is what makes adding one tractable.</p>

<p>Run them against a real workload and tell us where they get in the way. Issues and pull requests go to <a href="https://github.com/ArcadeData/arcadedb-drivers">ArcadeData/arcadedb-drivers</a>.</p>

<ul>
  <li><strong>Repository:</strong> <a href="https://github.com/ArcadeData/arcadedb-drivers">github.com/ArcadeData/arcadedb-drivers</a></li>
  <li><strong>Documentation:</strong> <a href="https://docs.arcadedb.com/arcadedb/how-to/connectivity/drivers/native-drivers">Native drivers</a></li>
  <li><strong>Packages:</strong> <a href="https://pypi.org/project/arcadedb-driver/"><code class="language-plaintext highlighter-rouge">arcadedb-driver</code></a> and <a href="https://pypi.org/project/arcadedb-driver-grpc/"><code class="language-plaintext highlighter-rouge">arcadedb-driver-grpc</code></a> on PyPI, <a href="https://www.npmjs.com/package/@arcadedb/driver"><code class="language-plaintext highlighter-rouge">@arcadedb/driver</code></a> and <a href="https://www.npmjs.com/package/@arcadedb/driver-grpc"><code class="language-plaintext highlighter-rouge">@arcadedb/driver-grpc</code></a> on npm</li>
</ul>]]></content><author><name>Roberto Franchini</name></author><category term="Drivers" /><category term="Python" /><category term="TypeScript" /><category term="JavaScript" /><category term="gRPC" /><category term="HTTP" /><category term="OpenAPI" /><category term="Multi-Model" /><category term="Graph Database" /><category term="ArcadeDB" /><summary type="html"><![CDATA[ArcadeDB now ships four native drivers: HTTP and gRPC clients for Python and for TypeScript/JavaScript, all generated from one OpenAPI spec and one Protobuf contract, published on PyPI and npm under Apache-2.0.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/arcadedb-native-drivers.png" /><media:content medium="image" url="https://arcadedb.com/assets/images/arcadedb-native-drivers.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB 26.9.1: 27.6x Faster Backups, Query Correctness Fixes, and 6 Security Advisories</title><link href="https://arcadedb.com/blog/arcadedb-26-9-1/" rel="alternate" type="text/html" title="ArcadeDB 26.9.1: 27.6x Faster Backups, Query Correctness Fixes, and 6 Security Advisories" /><published>2026-09-03T00:00:00+00:00</published><updated>2026-09-03T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-26-9-1</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-26-9-1/"><![CDATA[<p><strong>ArcadeDB 26.9.1</strong> is the largest release ArcadeDB has ever shipped: <strong>992 issues and pull requests closed</strong> under the <a href="https://github.com/ArcadeData/arcadedb/milestone/59">26.9.1 milestone</a>, 675 issues and 317 PRs, out of 655 pull requests merged and <strong>1,500 commits</strong> since <a href="https://arcadedb.com/blog/arcadedb-26-8-1/">26.8.1</a>.</p>

<h2 id="at-a-glance">At a Glance</h2>

<table>
  <thead>
    <tr>
      <th>Area</th>
      <th>What changed</th>
      <th>The number</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Backup and restore</td>
      <td>Parallel compression, parallel restore, no flush suspension</td>
      <td><strong>27.6x</strong> faster backup, <strong>77%</strong> writer throughput during it</td>
    </tr>
    <tr>
      <td>Storage integrity</td>
      <td>Records larger than a page: lost updates, phantom rows, leaked chunks</td>
      <td><strong>16%</strong> of chunk slots were orphaned and never reclaimed</td>
    </tr>
    <tr>
      <td>Query correctness</td>
      <td><code class="language-plaintext highlighter-rouge">NOT IN</code>, <code class="language-plaintext highlighter-rouge">DISTINCT ... LIMIT</code>, multi-key <code class="language-plaintext highlighter-rouge">GROUP BY</code>, in-transaction range scans</td>
      <td>5 independent wrong-result defects</td>
    </tr>
    <tr>
      <td>Index usage</td>
      <td><code class="language-plaintext highlighter-rouge">IN (...)</code>, <code class="language-plaintext highlighter-rouge">BETWEEN</code>, composite prefix + <code class="language-plaintext highlighter-rouge">ORDER BY</code>, <code class="language-plaintext highlighter-rouge">@rid IN [...]</code></td>
      <td><strong>1143x</strong> on <code class="language-plaintext highlighter-rouge">@rid IN [...]</code> at 400k documents</td>
    </tr>
    <tr>
      <td>Vector search</td>
      <td>Constant-time open, scheduled rebuilds, adaptive <code class="language-plaintext highlighter-rouge">efSearch</code></td>
      <td>recall@10 back to <strong>0.92</strong> past 10,000 vectors</td>
    </tr>
    <tr>
      <td>Security</td>
      <td>6 advisories, pre-auth DoS on three wire protocols, regex backtracking</td>
      <td>all affecting <strong>26.8.1 and earlier</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Upgrading is strongly recommended for every deployment.</strong> There are breaking changes and behaviour changes; no schema migration is required, and no existing database is rewritten.</p>

<h2 id="major-highlights">Major Highlights</h2>

<h3 id="backups-are-276x-faster-and-no-longer-stall-the-database">Backups Are 27.6x Faster and No Longer Stall the Database</h3>

<p>A full backup ran single-threaded deflate at level 9, CPU-bound at 20-40 MB/s, and it <strong>suspended page flushing for the whole window</strong>: dirty pages piled up until <code class="language-plaintext highlighter-rouge">arcadedb.flushSuspendMaxDeferredRAM</code> was reached, committers were throttled, and LSM compaction was postponed. HA snapshot shipping and cluster verify did the same thing (<a href="https://github.com/ArcadeData/arcadedb/issues/6072">#6072</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6075">#6075</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6086">#6086</a>).</p>

<p>Measured on a 1.25 GB database:</p>

<table>
  <thead>
    <tr>
      <th>Measurement</th>
      <th>Before</th>
      <th>After</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full backup</td>
      <td>18.88 s</td>
      <td><strong>0.68 s</strong> (27.6x)</td>
    </tr>
    <tr>
      <td>Concurrent writer throughput during the backup</td>
      <td>4.3% of baseline</td>
      <td><strong>77%</strong> of baseline</td>
    </tr>
    <tr>
      <td>Restore</td>
      <td>2.9 s</td>
      <td><strong>0.68 s</strong></td>
    </tr>
    <tr>
      <td>Archive size</td>
      <td>323 MB</td>
      <td>348 MB (+7.5%)</td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>Parallel compression</strong>, tunable with <code class="language-plaintext highlighter-rouge">arcadedb.backup.compressionLevel</code> (new default <strong>1</strong>, was 9), <code class="language-plaintext highlighter-rouge">arcadedb.backup.compressionThreads</code> and <code class="language-plaintext highlighter-rouge">arcadedb.backup.maxMBPerSecond</code>. The ZIP format is unchanged and archives written by older versions restore normally.</li>
  <li><strong>Parallel restore</strong>, largest entry first, behind a 256 KB buffered read instead of <code class="language-plaintext highlighter-rouge">ZipInputStream</code>’s unbuffered 512-byte reads (<code class="language-plaintext highlighter-rouge">arcadedb.restore.threads</code>). Even the sequential path went from 5.16 s to 3.96 s.</li>
  <li><strong>A page-level copy-on-write snapshot replaces flush suspension</strong>: <code class="language-plaintext highlighter-rouge">arcadedb.pageSnapshotEnabled</code>, <code class="language-plaintext highlighter-rouge">arcadedb.pageSnapshotMaxRAM</code>, <code class="language-plaintext highlighter-rouge">arcadedb.pageSnapshotMaxSize</code>, <code class="language-plaintext highlighter-rouge">arcadedb.pageSnapshotSpillPath</code>. Backups, HA snapshot shipping and the <code class="language-plaintext highlighter-rouge">/checksums</code> endpoint take a point-in-time view without ever stopping the flusher.</li>
  <li><strong>Two JVM-wide stalls are gone with it.</strong> The deferred-flush backpressure gate was process-wide, so one database’s backlog stopped the flush thread for <strong>every</strong> database on the server (<a href="https://github.com/ArcadeData/arcadedb/issues/6200">#6200</a>), and <code class="language-plaintext highlighter-rouge">publishPages</code> blocked inside the global page-manager lock whenever the flush queue filled, serialising the commits of every database behind one database’s write burst (<a href="https://github.com/ArcadeData/arcadedb/issues/6259">#6259</a>). Both are per-database now.</li>
</ul>

<blockquote>
  <p>The trade is 7.5% archive size for 27.6x backup time. Set <code class="language-plaintext highlighter-rouge">arcadedb.backup.compressionLevel=9</code> if the archive size matters more to you than the backup window.</p>
</blockquote>

<h3 id="records-larger-than-a-page-lost-updates-phantom-rows-and-a-16-space-leak">Records Larger Than a Page: Lost Updates, Phantom Rows and a 16% Space Leak</h3>

<p>A record that outgrows its page is stored as a chunk chain or behind a placeholder pointer. Re-triaging <a href="https://github.com/ArcadeData/arcadedb/issues/5279">#5279</a> turned up a whole family of defects on that path, every one of them silent:</p>

<ul>
  <li><strong>A lost update.</strong> Two transactions updating the same placeholder-backed record (pointer on one page, content on another) both committed and one write vanished, with no <code class="language-plaintext highlighter-rouge">ConcurrentModificationException</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6141">#6141</a>). The content page is version-checked now, so the conflict is raised.</li>
  <li><strong>A record returned twice.</strong> A <code class="language-plaintext highlighter-rouge">SELECT</code> returned a placeholder-backed record under two different RIDs when its content had spilled into chunks, so <code class="language-plaintext highlighter-rouge">count(@rid)</code> reported 2 for one record (<a href="https://github.com/ArcadeData/arcadedb/issues/6196">#6196</a>).</li>
  <li><strong>A 16% space leak.</strong> <code class="language-plaintext highlighter-rouge">CRUDTest.multiUpdatesOverlap</code> ended with <strong>243,821 orphaned chunk slots out of 1,545,495</strong>, because a shrink ending exactly on a chunk boundary never freed the tail and nothing ever reclaimed them, although three code comments promised otherwise (<a href="https://github.com/ArcadeData/arcadedb/issues/6319">#6319</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6294">#6294</a>). <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> now sweeps them and reports <code class="language-plaintext highlighter-rouge">orphanedChunks</code> / <code class="language-plaintext highlighter-rouge">orphanedChunksReclaimed</code>.</li>
  <li><strong>False conflicts.</strong> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6217">#6217</a>), and eight threads rewriting different large records on one page got <code class="language-plaintext highlighter-rouge">ConcurrentModificationException</code>, five of eight exhausting <code class="language-plaintext highlighter-rouge">TX_RETRIES</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6129">#6129</a>). Chunked head slots take part in the disjoint-slot merge now, and a read validates only its own chain.</li>
  <li><strong>A permanent size ratchet.</strong> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6163">#6163</a>). A record that shrinks back inside its slot is collapsed to a plain record again (<a href="https://github.com/ArcadeData/arcadedb/issues/6178">#6178</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6286">#6286</a>).</li>
  <li><strong>A checker that reported a clean database.</strong> After <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> force-deleted a record with a broken chain, the placeholder pointing at it was left dangling, so <code class="language-plaintext highlighter-rouge">count(*)</code> said 8 and a scan said 7, permanently, while the report said <code class="language-plaintext highlighter-rouge">totalErrors: 0</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6292">#6292</a>).</li>
</ul>

<p>Free-space accounting was fixed with them (<a href="https://github.com/ArcadeData/arcadedb/issues/6154">#6154</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6339">#6339</a>), and a self-referencing edge-list chunk no longer hangs an ordinary traversal in a request thread (<a href="https://github.com/ArcadeData/arcadedb/issues/6278">#6278</a>).</p>

<h3 id="wrong-results-in-ordinary-sql">Wrong Results in Ordinary SQL</h3>

<p>Five independent defects, all of them returning a plausible answer to the wrong question:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">NOT IN</code> returned the <code class="language-plaintext highlighter-rouge">IN</code> result set.</strong> <code class="language-plaintext highlighter-rouge">WHERE prop NOT IN [...]</code> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6796">#6796</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">SELECT DISTINCT ... ORDER BY ... LIMIT n</code> returned fewer than <code class="language-plaintext highlighter-rouge">n</code> rows</strong>, because the Top-K bound was applied before deduplication (<a href="https://github.com/ArcadeData/arcadedb/issues/6923">#6923</a>).</li>
  <li><strong>A multi-key <code class="language-plaintext highlighter-rouge">GROUP BY</code> grouped on the last key only</strong>: the synthetic alias counter was <code class="language-plaintext highlighter-rouge">final int i = 0</code> outside the loop, so every key got the same alias (<a href="https://github.com/ArcadeData/arcadedb/issues/6924">#6924</a>). <strong><code class="language-plaintext highlighter-rouge">DISTINCT</code> was silently dropped</strong> whenever the statement also had <code class="language-plaintext highlighter-rouge">GROUP BY</code>, <code class="language-plaintext highlighter-rouge">UNWIND</code> or <code class="language-plaintext highlighter-rouge">expand()</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6925">#6925</a>).</li>
  <li><strong>An index range scan ignored its own transaction’s deletes.</strong> A range query inside a transaction returned rows deleted or re-keyed in that same transaction, including rows that did not match the <code class="language-plaintext highlighter-rouge">WHERE</code> clause (<a href="https://github.com/ArcadeData/arcadedb/issues/6927">#6927</a>).</li>
  <li><strong>An indexed range over non-ASCII text returned nothing.</strong> <code class="language-plaintext highlighter-rouge">BinaryComparator</code> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6997">#6997</a>).</li>
</ul>

<p>Also: the <code class="language-plaintext highlighter-rouge">??</code> null-coalescing operator always returned its right operand because the AST builder had no visitor for it (<a href="https://github.com/ArcadeData/arcadedb/issues/6393">#6393</a>); <code class="language-plaintext highlighter-rouge">WHERE @rid &gt; :param</code> returned nothing while the same RID as a literal worked, breaking RID-cursor paging (<a href="https://github.com/ArcadeData/arcadedb/issues/6188">#6188</a>); <code class="language-plaintext highlighter-rouge">@rid IN (SELECT ...)</code> never matched (<a href="https://github.com/ArcadeData/arcadedb/issues/7054">#7054</a>); <code class="language-plaintext highlighter-rouge">TRUNCATE TYPE</code> inside an explicit transaction committed the caller’s transaction from the inside, so <code class="language-plaintext highlighter-rouge">BEGIN; TRUNCATE TYPE; ROLLBACK</code> destroyed 1,000 records (<a href="https://github.com/ArcadeData/arcadedb/issues/6220">#6220</a>); and <code class="language-plaintext highlighter-rouge">EXPLAIN UPDATE ...</code> submitted as <code class="language-plaintext highlighter-rouge">sqlscript</code> <strong>executed the update</strong>, which ran unbounded for hours in production (<a href="https://github.com/ArcadeData/arcadedb/issues/6648">#6648</a>).</p>

<h3 id="indexes-are-used-where-they-were-not">Indexes Are Used Where They Were Not</h3>

<table>
  <thead>
    <tr>
      <th>Query shape</th>
      <th>Before</th>
      <th>After</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WHERE prop IN (v1, ..., vN)</code> (parenthesised literal list)</td>
      <td>full scan, ~350-400 rows/s, ~40 s per 15k batch</td>
      <td>index lookup (<a href="https://github.com/ArcadeData/arcadedb/issues/6640">#6640</a>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WHERE k1 = ? AND k2 = ? ORDER BY ts DESC LIMIT 1</code> on a composite index</td>
      <td>full scan</td>
      <td>composite prefix seek plus a directional range scan (<a href="https://github.com/ArcadeData/arcadedb/issues/6592">#6592</a>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WHERE n BETWEEN 15 AND 25</code></td>
      <td>full scan (while <code class="language-plaintext highlighter-rouge">n &gt; 15 AND n &lt; 25</code> used the index)</td>
      <td>index range (<a href="https://github.com/ArcadeData/arcadedb/issues/5966">#5966</a>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WHERE @rid IN [...]</code> on a type</td>
      <td>full type scan, 82.12 ms at 400k docs</td>
      <td>direct RID fetch, 0.07 ms (<strong>1143x</strong>) (<a href="https://github.com/ArcadeData/arcadedb/issues/5824">#5824</a>)</td>
    </tr>
    <tr>
      <td>Cypher <code class="language-plaintext highlighter-rouge">MATCH (n:A\|B {id:'a1'})</code></td>
      <td>scan of 1,000 records</td>
      <td>per-root index seeks (<a href="https://github.com/ArcadeData/arcadedb/issues/6397">#6397</a>)</td>
    </tr>
    <tr>
      <td>Cypher <code class="language-plaintext highlighter-rouge">MATCH (e:Child) WHERE e.id IN $ids</code>, index on the parent type</td>
      <td>label scan</td>
      <td>inherited <code class="language-plaintext highlighter-rouge">NodeIndexSeek</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/7021">#7021</a>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WHERE LOWER(x) BETWEEN ...</code> / <code class="language-plaintext highlighter-rouge">LOWER(x) IN [...]</code> on a <code class="language-plaintext highlighter-rouge">COLLATE CI</code> index</td>
      <td>full scan</td>
      <td>index range (<a href="https://github.com/ArcadeData/arcadedb/issues/6033">#6033</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6037">#6037</a>)</td>
    </tr>
  </tbody>
</table>

<p>Two more index defects worth naming: <code class="language-plaintext highlighter-rouge">INSERT</code> followed by <code class="language-plaintext highlighter-rouge">CREATE INDEX</code> <strong>in the same transaction</strong> produced an index that was readable, reported healthy by <code class="language-plaintext highlighter-rouge">CHECK DATABASE</code>, and missing the record (<a href="https://github.com/ArcadeData/arcadedb/issues/6324">#6324</a>); and a composite index mixing a scalar property with one <code class="language-plaintext highlighter-rouge">BY ITEM</code>/<code class="language-plaintext highlighter-rouge">BY KEY</code>/<code class="language-plaintext highlighter-rouge">BY VALUE</code> property was never updated when only the scalar changed (<a href="https://github.com/ArcadeData/arcadedb/issues/6934">#6934</a>).</p>

<h3 id="vector-search-opening-a-database-rebuilding-the-graph-and-recall">Vector Search: Opening a Database, Rebuilding the Graph, and Recall</h3>

<p>Most of this was measured and reported by <a href="https://github.com/tae898">@tae898</a> on real corpora.</p>

<ul>
  <li><strong>Opening a database is constant-time again.</strong> Every open parsed every page of every <code class="language-plaintext highlighter-rouge">LSM_VECTOR</code> index to rebuild the in-memory location map, about <strong>1.4 s at 10M vectors</strong>, even for a session that never searched. The map is materialised on first use now (<a href="https://github.com/ArcadeData/arcadedb/issues/6722">#6722</a>). A Graph Analytical View was rebuilt by a full graph scan on every open too, <strong>4.03 s at 1M vertices</strong> for an open-and-close with no query; the CSR is persisted at clean close with a freshness certificate and restored lazily (<a href="https://github.com/ArcadeData/arcadedb/issues/6583">#6583</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6632">#6632</a>).</li>
  <li><strong>Rebuilds stopped ambushing the first query.</strong> A session that inserted before its first search paid a full synchronous rebuild on the search thread: <strong>2,618 ms versus 215 ms</strong> on 20,000 vectors, <strong>128,543 ms</strong> at 1M (<a href="https://github.com/ArcadeData/arcadedb/issues/6772">#6772</a>). A persisted graph is reused as a prefix and only the gap is built (<a href="https://github.com/ArcadeData/arcadedb/issues/6655">#6655</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6798">#6798</a>). A single insert into a settled 50,000-vector index no longer triggers an 8-14 s rebuild 15 s later (<a href="https://github.com/ArcadeData/arcadedb/issues/6496">#6496</a>), and the rebuild threshold scales past 250,000 vectors where the linear delta scan was ~79% of query time (<a href="https://github.com/ArcadeData/arcadedb/issues/6797">#6797</a>).</li>
  <li><strong>Recall stopped falling off a cliff.</strong> The adaptive <code class="language-plaintext highlighter-rouge">efSearch</code> beam narrowed from 100 to 20 once an index passed 10,000 nodes: <strong>recall@10 fell from 0.9200 at 9,000 vectors to 0.5420 at 11,000</strong>, for under 1 ms saved. The beam widens with graph size now, and an explicit <code class="language-plaintext highlighter-rouge">efSearch: 100</code> is honored (<a href="https://github.com/ArcadeData/arcadedb/issues/6494">#6494</a>).</li>
  <li><strong>A selective filter makes search faster, not slower.</strong> A RID allow-list made search slower the narrower it was (p50 2.020 ms unfiltered, <strong>30.145 ms for 5 RIDs</strong>, against 0.090 ms for a direct pre-filter). A pre-filter plan scores the allowed vectors directly when the allow-list covers at most <code class="language-plaintext highlighter-rouge">VECTOR_INDEX_PREFILTER_MAX_SELECTIVITY</code> (default 20%) of the index (<a href="https://github.com/ArcadeData/arcadedb/issues/6502">#6502</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6514">#6514</a>).</li>
  <li><strong>Builds use the machine.</strong> Graph construction is 93.4% of a DEEP-10M build and ran on <code class="language-plaintext highlighter-rouge">availableProcessors()/2</code> threads with 31.04% of CPU burned in <code class="language-plaintext highlighter-rouge">LongAdder.add</code> on the distance path; striped counters cut 2-5x per lookup and the pool defaults to cores minus one, settable with <code class="language-plaintext highlighter-rouge">arcadedb.vectorIndex.graphBuildParallelism</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5577">#5577</a>). The location index went from ~90 to <strong>~32 bytes per live vector</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5588">#5588</a>).</li>
  <li><strong>Two silent wrong answers.</strong> A partial compaction of the sparse-vector index could permanently <strong>resurrect deleted documents or revert updates</strong>, because a merged segment got a globally new highest id and outranked a newer tombstone under “newest wins” (<a href="https://github.com/ArcadeData/arcadedb/issues/6379">#6379</a>); 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/5761">#5761</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6936">#6936</a>).</li>
</ul>

<h3 id="opencypher-a-correctness-sweep-then-neo4j-compatibility">openCypher: a Correctness Sweep, Then Neo4j Compatibility</h3>

<p>A large batch of wrong-result defects came from differential fuzzing against Neo4j and Memgraph by <a href="https://github.com/YGY-001">@YGY-001</a> and <a href="https://github.com/shulei5831sl">@shulei5831sl</a>, plus follow-ups. The shape is always the same: the identical query written two ways gives two answers.</p>

<ul>
  <li><strong>An edge variable read only inside a list predicate was anonymised</strong>, because the reference check scanned whitespace-stripped text instead of the AST, so the <code class="language-plaintext highlighter-rouge">WHERE</code> read a missing binding and <strong>dropped every row</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/6567">#6567</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6599">#6599</a>); the same check had no <code class="language-plaintext highlighter-rouge">CREATE</code>/<code class="language-plaintext highlighter-rouge">MERGE</code> case, so <code class="language-plaintext highlighter-rouge">CREATE (c {since: r.since})</code> wrote null (<a href="https://github.com/ArcadeData/arcadedb/issues/6573">#6573</a>).</li>
  <li><strong>Relationship uniqueness was scoped to one pattern part</strong> instead of the whole <code class="language-plaintext highlighter-rouge">MATCH</code> clause, so the same <code class="language-plaintext highlighter-rouge">OPTIONAL MATCH</code> returned different row counts depending on whether the rows went through <code class="language-plaintext highlighter-rouge">collect</code>/<code class="language-plaintext highlighter-rouge">UNWIND</code> first (<a href="https://github.com/ArcadeData/arcadedb/issues/6310">#6310</a>).</li>
  <li><strong>A label disjunction <code class="language-plaintext highlighter-rouge">(y:A|B)</code> on a node bound by expansion matched nothing</strong> because the target-side check ANDed the alternatives (<a href="https://github.com/ArcadeData/arcadedb/issues/6338">#6338</a>), a backticked label in a <code class="language-plaintext highlighter-rouge">WHERE</code> never matched (<a href="https://github.com/ArcadeData/arcadedb/issues/6345">#6345</a>), and <code class="language-plaintext highlighter-rouge">labels()</code> dropped a vertex’s own type under inheritance (<a href="https://github.com/ArcadeData/arcadedb/issues/6363">#6363</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">MERGE</code> created duplicates</strong> when the anchor vertex was bound earlier in the same query, breaking idempotency on the most common graph-building shape (<a href="https://github.com/ArcadeData/arcadedb/issues/6461">#6461</a>).</li>
  <li><strong>A standalone leading <code class="language-plaintext highlighter-rouge">OPTIONAL MATCH</code> with more than 100 matches never terminated</strong>, re-running its scan from scratch on every pull batch and emitting the first 100 rows for ever (<a href="https://github.com/ArcadeData/arcadedb/issues/6668">#6668</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">MATCH p=(a)-[*1..N]-&gt;(b)</code> materialized every path</strong> and exhausted a 512 MB heap on a modest fan-out graph where SQL <code class="language-plaintext highlighter-rouge">TRAVERSE</code> answered in under a second; variable-length traversal is a lazy DFS generator now (<a href="https://github.com/ArcadeData/arcadedb/issues/6097">#6097</a>), and the cost-based optimizer plans it instead of falling back to the legacy executor (<a href="https://github.com/ArcadeData/arcadedb/issues/5358">#5358</a>).</li>
</ul>

<p>On the compatibility side: <strong>12 APOC-compatible functions and procedures</strong> including <code class="language-plaintext highlighter-rouge">apoc.refactor.mergeNodes</code>, <code class="language-plaintext highlighter-rouge">apoc.refactor.cloneNodesWithRelationships</code> and <code class="language-plaintext highlighter-rouge">apoc.do.when</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6059">#6059</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6060">#6060</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6157">#6157</a>); <strong><code class="language-plaintext highlighter-rouge">db.index.fulltext.queryNodes</code> / <code class="language-plaintext highlighter-rouge">queryRelationships</code></strong> bring BM25 full-text search into Cypher (<a href="https://github.com/ArcadeData/arcadedb/issues/6729">#6729</a>); and the <strong>Neo4j 5 dynamic-label syntax</strong> <code class="language-plaintext highlighter-rouge">SET n:$(expr)</code> / <code class="language-plaintext highlighter-rouge">REMOVE n:$(expr)</code> is implemented, where it used to parse and then create a vertex type literally named <code class="language-plaintext highlighter-rouge">$(node.labels)</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/7059">#7059</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7093">#7093</a>).</p>

<h3 id="gql-quantified-path-patterns-phase-b">GQL: Quantified Path Patterns, Phase B</h3>

<p>Quantified Path Patterns beyond the single-relationship case (ISO/IEC 39075 §15.4) were rejected with <code class="language-plaintext highlighter-rouge">FeatureNotImplemented</code>. A parenthesised sub-pattern can now repeat with a quantifier, carry its own <code class="language-plaintext highlighter-rouge">WHERE</code> evaluated per repetition, bind group variables as <code class="language-plaintext highlighter-rouge">LIST&lt;NODE&gt;</code> / <code class="language-plaintext highlighter-rouge">LIST&lt;RELATIONSHIP&gt;</code>, and support grouped path assignment with relationship isomorphism enforced across the group (<a href="https://github.com/ArcadeData/arcadedb/issues/4531">#4531</a>).</p>

<p>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 <code class="language-plaintext highlighter-rouge">WHERE</code> could not see earlier bindings of the same repetition, and deep repetitions overflowed the stack at about 5,000 (they run iteratively to 20,000 now).</p>

<h3 id="the-postgres-wire-protocol-works-with-the-defaults-your-driver-uses">The Postgres Wire Protocol Works With the Defaults Your Driver Uses</h3>

<p>Twenty-eight defects, most of them reported by driver behaviour rather than by reading the spec:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">UPDATE</code> on a vertex type in autocommit</strong>, the JDBC, psycopg and Spark default, failed with <code class="language-plaintext highlighter-rouge">Transaction not active</code> while document and edge updates succeeded. A vertex can be modified outside a transaction now, and <code class="language-plaintext highlighter-rouge">UPDATE</code>/<code class="language-plaintext highlighter-rouge">DELETE</code>/<code class="language-plaintext highlighter-rouge">INSERT</code> in autocommit each run as one statement-level transaction (<a href="https://github.com/ArcadeData/arcadedb/issues/7096">#7096</a>, open as discussion <a href="https://github.com/ArcadeData/arcadedb/discussions/1588">#1588</a> since 2024).</li>
  <li><strong>An error inside <code class="language-plaintext highlighter-rouge">BEGIN</code> wedged the session permanently</strong>: <code class="language-plaintext highlighter-rouge">ReadyForQuery</code> never reported <code class="language-plaintext highlighter-rouge">'E'</code>, <code class="language-plaintext highlighter-rouge">COMMIT</code>/<code class="language-plaintext highlighter-rouge">ROLLBACK</code> were not recognised, and every further statement was silently swallowed (<a href="https://github.com/ArcadeData/arcadedb/issues/6457">#6457</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6543">#6543</a>).</li>
  <li><strong>JDBC fetch size silently truncated results to the first N rows</strong>: <code class="language-plaintext highlighter-rouge">PortalSuspended</code> was written before the rows and the portal removed, so the follow-up <code class="language-plaintext highlighter-rouge">Execute</code> found nothing (<a href="https://github.com/ArcadeData/arcadedb/issues/6458">#6458</a>).</li>
  <li><strong>pgjdbc’s sixth execution of a <code class="language-plaintext highlighter-rouge">PreparedStatement</code> served stale rows</strong>: re-<code class="language-plaintext highlighter-rouge">Bind</code>ing an already-executed named statement reused the portal without resetting <code class="language-plaintext highlighter-rouge">executed</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6660">#6660</a>).</li>
  <li><strong>Schema probes answered nothing.</strong> <code class="language-plaintext highlighter-rouge">WHERE 1=0</code> and <code class="language-plaintext highlighter-rouge">LIMIT 0</code> over a computed projection returned no <code class="language-plaintext highlighter-rouge">RowDescription</code> at all, which is what Spark, Tableau and several JDBC and BI tools send first (<a href="https://github.com/ArcadeData/arcadedb/issues/6156">#6156</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6185">#6185</a>).</li>
  <li><strong>An idle connection busy-polled its socket ten times a second</strong> because <code class="language-plaintext highlighter-rouge">readMessage()</code> never blocked, so N pooled connections cost 10N wakeups per second (<a href="https://github.com/ArcadeData/arcadedb/issues/6410">#6410</a>).</li>
</ul>

<p>MongoDB, Bolt, gRPC, GraphQL, Redis and Gremlin got the same treatment; see <a href="#wire-protocols">Wire Protocols</a> below.</p>

<h3 id="optional-mtls-on-the-raft-transport">Optional mTLS on the Raft Transport</h3>

<p>The gRPC transport between cluster nodes (AppendEntries, RequestVote, snapshot transfer) ran <strong>in plaintext with no peer authentication</strong>, so any host that could reach the port could inject log entries. Optional mTLS is configurable through <code class="language-plaintext highlighter-rouge">arcadedb.ha.tls.enabled</code>, <code class="language-plaintext highlighter-rouge">arcadedb.ha.tls.certChainFile</code>, <code class="language-plaintext highlighter-rouge">arcadedb.ha.tls.privateKeyFile</code>, <code class="language-plaintext highlighter-rouge">arcadedb.ha.tls.trustCertCollectionFile</code> and <code class="language-plaintext highlighter-rouge">arcadedb.ha.tls.mutualAuth</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/3890">#3890</a>).</p>

<p>Off by default. Startup fails fast if any PEM file is unreadable, <code class="language-plaintext highlighter-rouge">mutualAuth=false</code> 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.</p>

<blockquote>
  <p>Certificates are read from disk at startup only, so rotating them requires a restart.</p>
</blockquote>

<h2 id="security-advisories">Security Advisories</h2>

<p>This release closes <strong>six security advisories</strong>, each published in full (impact, affected versions and credit) as a <a href="https://github.com/ArcadeData/arcadedb/security/advisories">GitHub Security Advisory</a> on the repository. All six affect <strong>26.8.1 and earlier</strong> and are patched in 26.9.1.</p>

<p><strong>Per-type ACL enforcement</strong></p>

<ul>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-wjhv-79gv-2pqg">GHSA-wjhv-79gv-2pqg</a></strong> (high): TimeSeries <code class="language-plaintext highlighter-rouge">types</code> ACL entries were not enforced on the write paths. Reported by <a href="https://github.com/FEARIS2">@FEARIS2</a>.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-2c8m-q484-jv7m">GHSA-2c8m-q484-jv7m</a></strong> (high): index-target and TimeSeries reads, writes and counts reached records without the bucket-level permission check. Reported by <a href="https://github.com/ruispereira">@ruispereira</a>.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-27vw-j8qc-5h7x">GHSA-27vw-j8qc-5h7x</a></strong> (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 <a href="https://github.com/manus-use">@manus-use</a>.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-chrr-vr3p-crcc">GHSA-chrr-vr3p-crcc</a></strong> (medium): the AI Chat <code class="language-plaintext highlighter-rouge">query_database</code> tool bypassed per-type and per-bucket ACL enforcement. Reported by <a href="https://github.com/T4ran24">@T4ran24</a>.</li>
</ul>

<p><strong>Untrusted input reaching the host</strong></p>

<ul>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-67m7-7w7g-mpmh">GHSA-67m7-7w7g-mpmh</a></strong> (high): the <code class="language-plaintext highlighter-rouge">IMPORT DATABASE</code> 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 <a href="https://github.com/tonghuaroot">@tonghuaroot</a>.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-j57p-qmrh-v7xv">GHSA-j57p-qmrh-v7xv</a></strong> (high): the script-trigger sandbox’s <code class="language-plaintext highlighter-rouge">DENIED</code> entry for <code class="language-plaintext highlighter-rouge">java.util.ResourceBundle</code> was bypassed by its subclasses, allowing classpath credential disclosure. Reported by <a href="https://github.com/baeseungwon1010">@baeseungwon1010</a>.</li>
</ul>

<p>Three further advisories were published just after the 26.8.1 release and are fixed in <strong>26.8.1</strong>, not here: <a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-rv64-62hr-wv2p">GHSA-rv64-62hr-wv2p</a> (<a href="https://nvd.nist.gov/vuln/detail/CVE-2026-76223">CVE-2026-76223</a>), <a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-wcm5-4wjm-9wj3">GHSA-wcm5-4wjm-9wj3</a> and <a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-mmww-w3w3-6r86">GHSA-mmww-w3w3-6r86</a>.</p>

<h3 id="also-hardened-in-this-release">Also Hardened in This Release</h3>

<p><strong>Pre-authentication denial of service on the wire protocols.</strong> An unauthenticated client could exhaust the server with a handful of bytes on three protocols, all found by <a href="https://github.com/ruispereira">@ruispereira</a>:</p>

<ul>
  <li>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 <strong>before the handshake</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5894">#5894</a>); the PackStream decoder did the same from a 32-bit length and recursed without a depth limit (<a href="https://github.com/ArcadeData/arcadedb/issues/5918">#5918</a>); and <code class="language-plaintext highlighter-rouge">LIST_8</code>/<code class="language-plaintext highlighter-rouge">LIST_16</code>/<code class="language-plaintext highlighter-rouge">MAP_16</code> element counts bypassed those guards while <code class="language-plaintext highlighter-rouge">ListFrame</code> allocated eagerly, so a <strong>~3 KB message could force ~256 MB of live heap</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/6800">#6800</a>). New bounds: <code class="language-plaintext highlighter-rouge">arcadedb.bolt.websocket.maxFrameSize</code> (16 MB), <code class="language-plaintext highlighter-rouge">arcadedb.bolt.maxMessageSize</code> (16 MB), <code class="language-plaintext highlighter-rouge">arcadedb.bolt.packstream.maxValueLength</code>, <code class="language-plaintext highlighter-rouge">maxElements</code>, <code class="language-plaintext highlighter-rouge">maxDepth</code>.</li>
  <li>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 (<a href="https://github.com/ArcadeData/arcadedb/issues/5895">#5895</a>). New bounds: <code class="language-plaintext highlighter-rouge">arcadedb.redis.maxMultiBulkDepth</code> (32), <code class="language-plaintext highlighter-rouge">maxMultiBulkLength</code>, <code class="language-plaintext highlighter-rouge">maxBulkLength</code>.</li>
  <li>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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6377">#6377</a>); the listener also accepted unbounded pre-auth connections (<a href="https://github.com/ArcadeData/arcadedb/issues/6412">#6412</a>). Redis and Bolt got the same window, and TCP keepalive is enabled on server sockets so a half-open connection is dropped by the OS rather than pinning a thread for ever (<a href="https://github.com/ArcadeData/arcadedb/issues/6761">#6761</a>).</li>
</ul>

<p><strong>Catastrophic regex backtracking.</strong> SQL <code class="language-plaintext highlighter-rouge">MATCHES</code> and openCypher <code class="language-plaintext highlighter-rouge">=~</code> handed a user pattern straight to <code class="language-plaintext highlighter-rouge">java.util.regex</code> with no bound, so <code class="language-plaintext highlighter-rouge">(.*a){20}$</code> on a 41-character string pinned a query thread indefinitely and <code class="language-plaintext highlighter-rouge">arcadedb.command.timeout</code> could not stop it. The new <code class="language-plaintext highlighter-rouge">arcadedb.command.regexTimeout</code> (default 1000 ms) bounds every regex evaluation independently of the command timeout (<a href="https://github.com/ArcadeData/arcadedb/issues/5886">#5886</a>). Parser recursion is bounded too, in Cypher, SQL and GraphQL (<a href="https://github.com/ArcadeData/arcadedb/issues/5851">#5851</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5853">#5853</a>).</p>

<p><strong>Other hardening</strong></p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">restore database &lt;url&gt;</code> followed redirects with no per-hop revalidation</strong>, so the one-shot host check was bypassed by a <code class="language-plaintext highlighter-rouge">3xx</code> redirect or DNS rebinding to an internal address (<a href="https://github.com/ArcadeData/arcadedb/issues/6381">#6381</a>). The two independent SSRF checks on <code class="language-plaintext highlighter-rouge">import database</code> also read two different configuration keys, so the documented opt-out worked for only one of them (<a href="https://github.com/ArcadeData/arcadedb/issues/6474">#6474</a>).</li>
  <li><strong>Revoked database-level grants stayed in effect until restart.</strong> A group’s <code class="language-plaintext highlighter-rouge">updateSchema</code>, <code class="language-plaintext highlighter-rouge">updateSecurity</code> and <code class="language-plaintext highlighter-rouge">updateDatabaseSettings</code> grants and its <code class="language-plaintext highlighter-rouge">resultSetLimit</code>/<code class="language-plaintext highlighter-rouge">readTimeout</code> were frozen at the values seen when the user first touched the database (<a href="https://github.com/ArcadeData/arcadedb/issues/6806">#6806</a>).</li>
  <li><strong>The polyglot (JS) engine kept script parameters bound in the shared context</strong> after each command, so a later <code class="language-plaintext highlighter-rouge">js</code> command from any caller could read a previous caller’s parameters and globals (<a href="https://github.com/ArcadeData/arcadedb/issues/6759">#6759</a>), and the host-class allow-list’s ancestor walk skipped package-wildcard <code class="language-plaintext highlighter-rouge">DENIED</code> entries (<a href="https://github.com/ArcadeData/arcadedb/issues/6045">#6045</a>).</li>
  <li><strong>Credentials stopped being written down.</strong> The console wrote every <code class="language-plaintext highlighter-rouge">connect remote: ... &lt;password&gt;</code> and <code class="language-plaintext highlighter-rouge">create user ... identified by &lt;password&gt;</code> line to <code class="language-plaintext highlighter-rouge">./.history</code> in cleartext and echoed it in <code class="language-plaintext highlighter-rouge">-b</code> mode (<a href="https://github.com/ArcadeData/arcadedb/issues/6829">#6829</a>), and with <code class="language-plaintext highlighter-rouge">arcadedb.bolt.debug=true</code> the HELLO message logged the caller’s cleartext password (<a href="https://github.com/ArcadeData/arcadedb/issues/6801">#6801</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">POST /api/v1/login</code> minted a session per call into an unbounded map</strong>, storing untruncated client-controlled headers for at least 30 minutes (<a href="https://github.com/ArcadeData/arcadedb/issues/6809">#6809</a>), and the <code class="language-plaintext highlighter-rouge">db</code> tag of the <code class="language-plaintext highlighter-rouge">arcadedb.http.requests</code> meter was the raw path parameter with no existence check, so unauthenticated requests grew the meter registry without bound (<a href="https://github.com/ArcadeData/arcadedb/issues/6805">#6805</a>).</li>
  <li><strong>Bolt <code class="language-plaintext highlighter-rouge">LOGOFF</code> was accepted in any state</strong> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6803">#6803</a>).</li>
  <li><strong>The Gremlin shaded jar bundled Jackson 2.15.2.</strong> TinkerPop’s <code class="language-plaintext highlighter-rouge">gremlin-shaded</code> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/7097">#7097</a>).</li>
  <li><strong>WAL recovery allocated a page array straight from a file-read count</strong>, so a corrupt page-count field produced an <code class="language-plaintext highlighter-rouge">OutOfMemoryError</code> the recovery guard could not catch (<a href="https://github.com/ArcadeData/arcadedb/issues/6932">#6932</a>), and PromQL <code class="language-plaintext highlighter-rouge">query_range</code> overflowed its step-count guard and wedged an Undertow worker in an unbounded loop (<a href="https://github.com/ArcadeData/arcadedb/issues/6807">#6807</a>).</li>
</ul>

<h2 id="new-features">New Features</h2>

<h3 id="sql">SQL</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">INSERT ... ON DUPLICATE KEY SKIP</code></strong>: 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 <code class="language-plaintext highlighter-rouge">@skipped: true</code>, the offending index and the key; works with <code class="language-plaintext highlighter-rouge">CONTENT</code>, <code class="language-plaintext highlighter-rouge">SET</code> and <code class="language-plaintext highlighter-rouge">INSERT ... FROM &lt;query&gt;</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/4918">#4918</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX RECLAIM UNREFERENCED FILES</code></strong> deletes files with no schema component, left behind by an abandoned HA schema instalment sequence, and reports them (<a href="https://github.com/ArcadeData/arcadedb/issues/6189">#6189</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CHECK DATABASE ... DEEP</code></strong> is a new tier for the expensive TimeSeries sealed-store checks, with a <code class="language-plaintext highlighter-rouge">FIX</code> arm that repairs what is derived from the sealed blocks (<a href="https://github.com/ArcadeData/arcadedb/issues/6360">#6360</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">SQLFunction#isDeterministic()</code></strong> lets a function opt into plan caching and constant folding; <code class="language-plaintext highlighter-rouge">abs</code>, <code class="language-plaintext highlighter-rouge">pow</code>, <code class="language-plaintext highlighter-rouge">sqrt</code>, <code class="language-plaintext highlighter-rouge">coalesce</code>, <code class="language-plaintext highlighter-rouge">ifnull</code>, <code class="language-plaintext highlighter-rouge">ifempty</code>, <code class="language-plaintext highlighter-rouge">if</code>, <code class="language-plaintext highlighter-rouge">decode</code> and <code class="language-plaintext highlighter-rouge">strcmpci</code> do (<a href="https://github.com/ArcadeData/arcadedb/issues/6190">#6190</a>).</li>
</ul>

<h3 id="query-languages">Query Languages</h3>

<ul>
  <li><strong>GQL Quantified Path Patterns Phase B</strong>, see the highlight above (<a href="https://github.com/ArcadeData/arcadedb/issues/4531">#4531</a>).</li>
  <li><strong>12 APOC-compatible Cypher functions and procedures</strong>: <code class="language-plaintext highlighter-rouge">coll.sum</code>, <code class="language-plaintext highlighter-rouge">coll.avg</code>, <code class="language-plaintext highlighter-rouge">coll.union</code>, <code class="language-plaintext highlighter-rouge">coll.unionAll</code>, <code class="language-plaintext highlighter-rouge">coll.toSet</code>, <code class="language-plaintext highlighter-rouge">coll.pairsMin</code>, <code class="language-plaintext highlighter-rouge">math.round</code>, <code class="language-plaintext highlighter-rouge">convert.toString</code>, <code class="language-plaintext highlighter-rouge">number.format</code>, <code class="language-plaintext highlighter-rouge">apoc.do.when</code>, <code class="language-plaintext highlighter-rouge">apoc.refactor.mergeNodes</code> and <code class="language-plaintext highlighter-rouge">apoc.refactor.cloneNodesWithRelationships</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">db.index.fulltext.queryNodes</code> / <code class="language-plaintext highlighter-rouge">db.index.fulltext.queryRelationships</code></strong> YIELD <code class="language-plaintext highlighter-rouge">(node|relationship, score)</code> from ArcadeDB’s BM25 <code class="language-plaintext highlighter-rouge">FULL_TEXT</code> index inside a Cypher statement, matching Neo4j (<a href="https://github.com/ArcadeData/arcadedb/issues/6729">#6729</a>).</li>
  <li><strong>Cypher dynamic labels</strong> <code class="language-plaintext highlighter-rouge">SET n:$(expr)</code> and <code class="language-plaintext highlighter-rouge">REMOVE n:$(expr)</code>, plus <code class="language-plaintext highlighter-rouge">REMOVE n IS Label</code>.</li>
  <li><strong>The GQL standalone <code class="language-plaintext highlighter-rouge">FILTER</code> clause</strong> actually filters (<a href="https://github.com/ArcadeData/arcadedb/issues/6574">#6574</a>).</li>
</ul>

<h3 id="server-and-operations">Server and Operations</h3>

<ul>
  <li><strong>Optional mTLS on the Raft gRPC transport</strong>, see the highlight above (<a href="https://github.com/ArcadeData/arcadedb/issues/3890">#3890</a>).</li>
  <li><strong>A per-protocol HA routing table.</strong> <code class="language-plaintext highlighter-rouge">getRoutingTable(ROUTING_PROTOCOL)</code> and a <code class="language-plaintext highlighter-rouge">grpc:</code> field in <code class="language-plaintext highlighter-rouge">arcadedb.ha.serverList</code>, so a follower refusing <code class="language-plaintext highlighter-rouge">graphBatchLoad</code> can name a dialable gRPC address in the <code class="language-plaintext highlighter-rouge">arcadedb-leader-grpc-address</code> trailer instead of only the leader’s HTTP address (<a href="https://github.com/ArcadeData/arcadedb/issues/6091">#6091</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">/api/v1/cluster</code> reports live Raft membership.</strong> Every peer carries <code class="language-plaintext highlighter-rouge">inConfiguration</code>, a declared peer the cluster no longer contains reports role <code class="language-plaintext highlighter-rouge">NOT_IN_CONFIGURATION</code>, the divergence raises <code class="language-plaintext highlighter-rouge">peers-not-in-configuration</code> / <code class="language-plaintext highlighter-rouge">peers-not-in-server-list</code> alerts, and Studio shows the state (<a href="https://github.com/ArcadeData/arcadedb/issues/7040">#7040</a>).</li>
  <li><strong>A skip mode for the bulk importer.</strong> <code class="language-plaintext highlighter-rouge">-onRowError skip|abort</code> (default <code class="language-plaintext highlighter-rouge">abort</code>) logs and skips a malformed or out-of-range row instead of aborting the whole job, counting it in the summary (<a href="https://github.com/ArcadeData/arcadedb/issues/5968">#5968</a>).</li>
  <li><strong>Exponential backoff with full jitter for transaction retries</strong>, starting from the new <code class="language-plaintext highlighter-rouge">arcadedb.txRetryDelayBase</code> and doubling per attempt up to the <code class="language-plaintext highlighter-rouge">arcadedb.txRetryDelay</code> cap, instead of drawing from the same flat window on every attempt (<a href="https://github.com/ArcadeData/arcadedb/issues/5587">#5587</a>).</li>
  <li><strong>The OpenAPI spec is a publishable, self-identifying contract</strong>, smoke-tested against a TypeScript client generated from the server built in the same commit (<a href="https://github.com/ArcadeData/arcadedb/issues/4894">#4894</a>).</li>
</ul>

<h2 id="major-fixes-and-improvements">Major Fixes and Improvements</h2>

<h3 id="storage-and-integrity">Storage and Integrity</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">CHECK DATABASE</code> at scale.</strong> A hub vertex’s adjacency list was re-walked once per edge, O(degree²) on super-nodes: one <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> on a real <strong>657 GB</strong> graph measured <strong>80h19m</strong>. A per-pass probe cache takes that to O(degree) (<a href="https://github.com/ArcadeData/arcadedb/issues/6062">#6062</a>). Orphan edge records are named and reclaimed (<a href="https://github.com/ArcadeData/arcadedb/issues/6090">#6090</a>), and repairs are budgeted and committed in batches instead of stopping (<a href="https://github.com/ArcadeData/arcadedb/issues/6320">#6320</a>).</li>
  <li><strong>A fenced database no longer hangs with “No flush progress for 60000 ms”</strong>, reported twice from production during a <code class="language-plaintext highlighter-rouge">GraphBatch</code> import and a <code class="language-plaintext highlighter-rouge">DELETE ... BATCH</code> loop. A database fenced after a failed post-WAL commit stranded queued page-flush acks (<a href="https://github.com/ArcadeData/arcadedb/issues/6505">#6505</a>).</li>
  <li><strong>Renaming a vertex type broke every subsequent edge insert</strong> on that type with <code class="language-plaintext highlighter-rouge">SchemaException: Bucket with name 'Human_0_out_edges' was not found</code>, 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6667">#6667</a>).</li>
  <li><strong>A forward bucket scan fetched one page past the end</strong>, 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6014">#6014</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6015">#6015</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">removeSuperType()</code> withdrew only the type’s own buckets</strong> from the ancestor’s polymorphic cache while <code class="language-plaintext highlighter-rouge">linkSuperType()</code> had contributed the whole subtree, so after unlinking B from A a grandchild’s records still came back from <code class="language-plaintext highlighter-rouge">SELECT FROM A</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6935">#6935</a>).</li>
  <li><strong>A date pattern with <code class="language-plaintext highlighter-rouge">MMM</code>/<code class="language-plaintext highlighter-rouge">EEE</code> rendered month names in the JVM default locale</strong>, so a schema date written on an <code class="language-plaintext highlighter-rouge">it_IT</code> node failed to parse on another, and <code class="language-plaintext highlighter-rouge">FileUtils.copyFile</code> ignored <code class="language-plaintext highlighter-rouge">transferTo</code>’s return value so a file over 2 GB was silently truncated (<a href="https://github.com/ArcadeData/arcadedb/issues/7112">#7112</a>).</li>
</ul>

<h3 id="indexes">Indexes</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">CREATE INDEX &lt;name&gt; IF NOT EXISTS</code> answered <code class="language-plaintext highlighter-rouge">created: true</code> under the requested name while silently reusing a pre-existing index</strong> on the same property, so <code class="language-plaintext highlighter-rouge">SEARCH_INDEX('&lt;name&gt;', :q)</code> later failed or ranked nothing (<a href="https://github.com/ArcadeData/arcadedb/issues/6921">#6921</a>).</li>
  <li><strong>Index configuration is no longer lost on the repair and restore paths.</strong> <code class="language-plaintext highlighter-rouge">TRUNCATE TYPE</code>, <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code>, adding a bucket and adding a supertype recreated <code class="language-plaintext highlighter-rouge">FULL_TEXT</code>, geospatial and <code class="language-plaintext highlighter-rouge">LSM_SPARSE_VECTOR</code> indexes from the underlying LSM-Tree’s metadata, so analyzers, BM25 parameters, geohash resolution and sparse-vector settings silently reverted to defaults (<a href="https://github.com/ArcadeData/arcadedb/issues/5742">#5742</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5934">#5934</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">REBUILD INDEX</code> no longer returns silently when it fails.</strong> Both it and <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> retried the whole drop-and-create body, so a failure after the drop had committed could leave the index permanently missing (<a href="https://github.com/ArcadeData/arcadedb/issues/6040">#6040</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CONTAINSTEXT</code> on a single-property full-text index split its literal on <code class="language-plaintext highlighter-rouge">:</code></strong>, so any value containing a colon returned no matches (<a href="https://github.com/ArcadeData/arcadedb/issues/6382">#6382</a>); two <code class="language-plaintext highlighter-rouge">CONTAINSTEXT</code> conditions on the same property sent only the first to the index (<a href="https://github.com/ArcadeData/arcadedb/issues/6427">#6427</a>); and a field-qualified phrase query ignored its field (<a href="https://github.com/ArcadeData/arcadedb/issues/7000">#7000</a>).</li>
  <li><strong>An <code class="language-plaintext highlighter-rouge">LSM_TREE</code> index stores a <code class="language-plaintext highlighter-rouge">LINK</code> key as a compressed RID</strong> of about 2-7 bytes instead of a fixed 12 per column, roughly halving the key bytes of an <code class="language-plaintext highlighter-rouge">(@out, @in)</code> edge de-duplication index (<a href="https://github.com/ArcadeData/arcadedb/issues/5703">#5703</a>).</li>
  <li><strong>An index cursor allocates 6-8 fewer short-lived objects per row</strong> on a unique-index range scan, about 7M objects saved on a 1M-row scan (<a href="https://github.com/ArcadeData/arcadedb/issues/6944">#6944</a>).</li>
</ul>

<h3 id="numeric-correctness">Numeric Correctness</h3>

<p>A family of unchecked narrowings, all found by <a href="https://github.com/ruispereira">@ruispereira</a>, all silent:</p>

<ul>
  <li><strong>Storing an out-of-range value in an <code class="language-plaintext highlighter-rouge">INTEGER</code>/<code class="language-plaintext highlighter-rouge">SHORT</code>/<code class="language-plaintext highlighter-rouge">BYTE</code> property wrapped it</strong>: <code class="language-plaintext highlighter-rouge">SET n = 3000000000</code> stored <code class="language-plaintext highlighter-rouge">-1294967296</code> with no error (<a href="https://github.com/ArcadeData/arcadedb/issues/5905">#5905</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">SUM()</code>/<code class="language-plaintext highlighter-rouge">AVG()</code> over an <code class="language-plaintext highlighter-rouge">INTEGER</code> column overflowed silently</strong> once the running sum passed <code class="language-plaintext highlighter-rouge">Integer.MAX_VALUE</code>: five rows of 2,000,000,000 gave <code class="language-plaintext highlighter-rouge">sum = 5705032704</code> instead of 10,000,000,000 (<a href="https://github.com/ArcadeData/arcadedb/issues/5906">#5906</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">LIMIT 2147483648</code> narrowed to <code class="language-plaintext highlighter-rouge">Integer.MIN_VALUE</code> and returned 0 rows</strong>, a finite <code class="language-plaintext highlighter-rouge">double</code> above <code class="language-plaintext highlighter-rouge">Float.MAX_VALUE</code> was dropped from map JSON, and a <code class="language-plaintext highlighter-rouge">DOUBLE</code> MIN/MAX constraint was checked as <code class="language-plaintext highlighter-rouge">float</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5919">#5919</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">BinaryComparator</code> narrowed the wider operand to the first operand’s width</strong>, giving a non-antisymmetric order, and parsed string operands with <code class="language-plaintext highlighter-rouge">Integer.parseInt</code>, so <code class="language-plaintext highlighter-rouge">WHERE n &lt; 'abc'</code> crashed (<a href="https://github.com/ArcadeData/arcadedb/issues/5900">#5900</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">NaN</code> narrowed to <code class="language-plaintext highlighter-rouge">0</code></strong> when converting a <code class="language-plaintext highlighter-rouge">Double</code>/<code class="language-plaintext highlighter-rouge">Float</code> to an integral type, scalar and array paths alike (<a href="https://github.com/ArcadeData/arcadedb/issues/5970">#5970</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6020">#6020</a>).</li>
</ul>

<h3 id="sql-1">SQL</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">split()</code> returns a <code class="language-plaintext highlighter-rouge">String[]</code>, and the operator surface now handles it.</strong> <code class="language-plaintext highlighter-rouge">CONTAINS</code> on either side, <code class="language-plaintext highlighter-rouge">CONTAINSANY</code>, and <code class="language-plaintext highlighter-rouge">join()</code>/<code class="language-plaintext highlighter-rouge">sort()</code>/<code class="language-plaintext highlighter-rouge">first()</code>/<code class="language-plaintext highlighter-rouge">last()</code>/<code class="language-plaintext highlighter-rouge">asList()</code> all mishandled a plain array: <code class="language-plaintext highlighter-rouge">'a b c'.split(' ') CONTAINS 'a'</code> was false, <code class="language-plaintext highlighter-rouge">join()</code> leaked <code class="language-plaintext highlighter-rouge">[Ljava.lang.String;@7a8fa663</code>, <code class="language-plaintext highlighter-rouge">sort()</code> returned the input unsorted (<a href="https://github.com/ArcadeData/arcadedb/issues/6984">#6984</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7084">#7084</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">sum</code>/<code class="language-plaintext highlighter-rouge">avg</code>/<code class="language-plaintext highlighter-rouge">min</code>/<code class="language-plaintext highlighter-rouge">max</code> over zero matching rows</strong> returned an empty result set while <code class="language-plaintext highlighter-rouge">count(*)</code> returned one row with 0; they return one null row now, per ANSI SQL (<a href="https://github.com/ArcadeData/arcadedb/issues/6680">#6680</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">astar()</code> computed every heuristic cost as if the node were the start</strong>, so A* and <code class="language-plaintext highlighter-rouge">dijkstra()</code> with axis coordinates could return non-optimal paths (<a href="https://github.com/ArcadeData/arcadedb/issues/6385">#6385</a>).</li>
  <li><strong>62 SQL functions and methods threw raw JDK exceptions</strong> on missing, negative or wrong-typed arguments (<code class="language-plaintext highlighter-rouge">'abc'.substring()</code>, <code class="language-plaintext highlighter-rouge">left('abc', -1)</code>, <code class="language-plaintext highlighter-rouge">range([1], 3)</code>); arguments are validated and reported as HTTP 400 client errors now (<a href="https://github.com/ArcadeData/arcadedb/issues/5884">#5884</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5885">#5885</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">arcadedb.command.timeout</code> now bounds what it claims to.</strong> The deadline belongs to the <code class="language-plaintext highlighter-rouge">CommandContext</code>, inherited by subqueries, UNION branches and parallel scan workers, and is checked inside openCypher scans, expansions and joins, SQL <code class="language-plaintext highlighter-rouge">TRAVERSE</code>/<code class="language-plaintext highlighter-rouge">MATCH</code>/filter steps, pathfinding functions, WHERE-less aggregation scans and the vector k-NN path (<a href="https://github.com/ArcadeData/arcadedb/issues/6266">#6266</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6873">#6873</a>).</li>
  <li><strong>Schema probes are free.</strong> <code class="language-plaintext highlighter-rouge">WHERE 1=0</code> and <code class="language-plaintext highlighter-rouge">LIMIT 0</code> fold to an <code class="language-plaintext highlighter-rouge">EMPTY RESULT</code> step at plan time instead of scanning the target, and <code class="language-plaintext highlighter-rouge">WHERE 1=1</code> folds away instead of being evaluated per record (<a href="https://github.com/ArcadeData/arcadedb/issues/6174">#6174</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6184">#6184</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">SELECT FROM $var</code> mutated the cached statement’s target in place</strong>, so a later execution of the same SQL text with a different binding read the first execution’s type (<a href="https://github.com/ArcadeData/arcadedb/issues/6669">#6669</a>).</li>
  <li><strong>A property <code class="language-plaintext highlighter-rouge">DEFAULT</code> that failed to parse was silently stored as its own source text</strong> on every record, and re-parsed on every insert; defaults are validated at DDL time and parsed once (<a href="https://github.com/ArcadeData/arcadedb/issues/6134">#6134</a>).</li>
</ul>

<h3 id="graph-engine-and-analytical-views">Graph Engine and Analytical Views</h3>

<ul>
  <li><strong>A Graph Analytical View’s delta overlay is deletion-aware.</strong> Deleting one of several parallel edges masked all of them (<a href="https://github.com/ArcadeData/arcadedb/issues/6769">#6769</a>), an edge created and deleted inside the same overlay window still surfaced as live (<a href="https://github.com/ArcadeData/arcadedb/issues/6775">#6775</a>), and after a base vertex was deleted the dense node ids could exceed <code class="language-plaintext highlighter-rouge">getNodeCount()</code>, so every <code class="language-plaintext highlighter-rouge">algo.*</code> procedure silently skipped live vertices (<a href="https://github.com/ArcadeData/arcadedb/issues/6792">#6792</a>).</li>
  <li><strong>Super-node edge ordering.</strong> 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 <code class="language-plaintext highlighter-rouge">arcadedb.graph.supernodeInterleaveRounds</code> degrading to plain concatenation for a full walk (<a href="https://github.com/ArcadeData/arcadedb/issues/6044">#6044</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6064">#6064</a>).</li>
  <li><strong>All 22 superlinear <code class="language-plaintext highlighter-rouge">algo.*</code> procedures are abortable and budgeted.</strong> An O(V³) run the memory budget admitted ignored <code class="language-plaintext highlighter-rouge">Thread.interrupt()</code>, <code class="language-plaintext highlighter-rouge">arcadedb.command.timeout</code> and client cancellation (<a href="https://github.com/ArcadeData/arcadedb/issues/6302">#6302</a>); the graph an <code class="language-plaintext highlighter-rouge">algo.*</code> call loads, the embedding matrices and the <code class="language-plaintext highlighter-rouge">nodeCount²</code> bitsets are all priced against <code class="language-plaintext highlighter-rouge">arcadedb.cypher.algoMaxWorkingMemory</code> now (<a href="https://github.com/ArcadeData/arcadedb/issues/6317">#6317</a>).</li>
  <li><strong>Two <code class="language-plaintext highlighter-rouge">algo.*</code> wrong answers</strong>: <code class="language-plaintext highlighter-rouge">algo.steinerTree</code> and <code class="language-plaintext highlighter-rouge">algo.maxKCut</code> paired edge weights with neighbours by iteration position, so a <code class="language-plaintext highlighter-rouge">relTypes</code> filter or the mere presence of a Graph Analytical View produced wrong trees, weights and partitions (<code class="language-plaintext highlighter-rouge">totalWeight</code> 1000.0 for a tree costing 2.0) (<a href="https://github.com/ArcadeData/arcadedb/issues/6301">#6301</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6376">#6376</a>). <code class="language-plaintext highlighter-rouge">algo.wcc</code> ignored its <code class="language-plaintext highlighter-rouge">relTypes</code> argument and <code class="language-plaintext highlighter-rouge">algo.degree</code> ignored its <code class="language-plaintext highlighter-rouge">direction</code>.</li>
</ul>

<h3 id="bulk-load-and-the-async-executor">Bulk Load and the Async Executor</h3>

<ul>
  <li><strong>A JSONL batch load silently dropped vertices.</strong> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/5618">#5618</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">GraphBatch</code> retained 16-18 GB of caches for 100M distinct vertices</strong>, 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/5664">#5664</a>).</li>
  <li><strong>The async worker pool stopped being torn down and respawned.</strong> <code class="language-plaintext highlighter-rouge">setTransactionUseWAL()</code>/<code class="language-plaintext highlighter-rouge">setTransactionSync()</code> recreated the whole pool, four times per <code class="language-plaintext highlighter-rouge">GraphBatch</code> flush, force-exiting every other user’s queued tasks (2,183 <code class="language-plaintext highlighter-rouge">InterruptedIOException</code>s in one production log); the flags are plain volatile writes now, and <code class="language-plaintext highlighter-rouge">setParallelLevel()</code> resizes in place (<a href="https://github.com/ArcadeData/arcadedb/issues/6509">#6509</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5665">#5665</a>).</li>
  <li><strong>Async writes behave like synchronous ones</strong>: <code class="language-plaintext highlighter-rouge">updateRecord()</code> never called <code class="language-plaintext highlighter-rouge">validate()</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/7002">#7002</a>), <code class="language-plaintext highlighter-rouge">deleteRecord()</code> fired every before- and after-delete listener twice (<a href="https://github.com/ArcadeData/arcadedb/issues/7003">#7003</a>), and <code class="language-plaintext highlighter-rouge">scanType()</code> returned normally when a bucket scan threw (<a href="https://github.com/ArcadeData/arcadedb/issues/6467">#6467</a>).</li>
  <li><strong>A truncated batch upload applied its records twice</strong>, the 409 being the duplicate-key mapping, so a client resuming from the reported counts double-inserted; the interrupted stream commits once and answers 408 with the real counts (<a href="https://github.com/ArcadeData/arcadedb/issues/6176">#6176</a>).</li>
</ul>

<h3 id="backup-export-and-import">Backup, Export and Import</h3>

<ul>
  <li><strong>JSONL export and import lost data silently.</strong> The exporter wrote DATE values as epoch milliseconds while the importer decoded them as epoch days, so <strong>every record with a modern DATE was dropped on import together with its edges</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/6455">#6455</a>); LINK property values were never remapped, so restored links pointed at unrelated records (<a href="https://github.com/ArcadeData/arcadedb/issues/6460">#6460</a>); and both sides logged per-record failures and reported success (<a href="https://github.com/ArcadeData/arcadedb/issues/6468">#6468</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6471">#6471</a>).</li>
  <li><strong>Two concurrent backups of the same database wrote the same second-precision path</strong>, producing a torn or overwritten archive; backups are serialised per database and the target path claimed atomically (<a href="https://github.com/ArcadeData/arcadedb/issues/6753">#6753</a>). An auto-backup schedule was never cancelled when its database was dropped or closed (<a href="https://github.com/ArcadeData/arcadedb/issues/6752">#6752</a>).</li>
  <li><strong>The OrientDB importer parsed every JSON number as <code class="language-plaintext highlighter-rouge">double</code></strong>, so LONG values above 2⁵³ were off by one, and it silently dropped composite indexes, losing UNIQUE constraints after migration (<a href="https://github.com/ArcadeData/arcadedb/issues/6749">#6749</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6750">#6750</a>).</li>
  <li><strong>Importing any ZIP source silently yielded 0 records and reported success</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/6810">#6810</a>), and a user-supplied CSV delimiter was overwritten with null (<a href="https://github.com/ArcadeData/arcadedb/issues/6811">#6811</a>).</li>
</ul>

<h3 id="ha-and-raft-clustering">HA and Raft Clustering</h3>

<ul>
  <li><strong>A replica-originated insert lost its unique-index entry on every node.</strong> The record committed and replicated cluster-wide, a full scan found it, <code class="language-plaintext highlighter-rouge">lookupByKey</code> never did, and a duplicate key could be inserted, because a replica committing its own transaction shipped only the record data (<a href="https://github.com/ArcadeData/arcadedb/issues/6964">#6964</a>).</li>
  <li><strong>A large, highly compressible bulk transaction crash-looped an entire cluster.</strong> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/5933">#5933</a>).</li>
  <li><strong>A snapshot install that gave up on one database still ACKed for all of them</strong>, 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6760">#6760</a>).</li>
  <li><strong>REST user management did not replicate.</strong> <code class="language-plaintext highlighter-rouge">POST</code>/<code class="language-plaintext highlighter-rouge">PUT</code>/<code class="language-plaintext highlighter-rouge">DELETE /api/v1/server/users</code> mutated the user store only on the node that served the request, while the equivalent <code class="language-plaintext highlighter-rouge">create user</code> command replicated through Raft, so a user created via REST got 401 on the other nodes (<a href="https://github.com/ArcadeData/arcadedb/issues/6808">#6808</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">RaftGroupCommitter</code> awaited each entry of a batch with the full quorum timeout</strong> sequentially on one thread, so an unresponsive quorum stalled replication for <strong>about 83 minutes</strong> at defaults (<a href="https://github.com/ArcadeData/arcadedb/issues/5848">#5848</a>).</li>
  <li><strong>A follower whose log writer hit <code class="language-plaintext highlighter-rouge">No space left on device</code> stayed RUNNING while rejecting every append</strong>, with nothing short of an operator restart recovering it; the health monitor restarts the server in place once the volume has room (<a href="https://github.com/ArcadeData/arcadedb/issues/7037">#7037</a>).</li>
  <li><strong>Self-dial loops on single-host clusters are closed.</strong> A follower whose derived leader address resolved to itself forwarded every write to itself in an unbounded loop (<a href="https://github.com/ArcadeData/arcadedb/issues/6191">#6191</a>), <code class="language-plaintext highlighter-rouge">localhost</code> and <code class="language-plaintext highlighter-rouge">127.0.0.1</code> were not recognised as the same endpoint (<a href="https://github.com/ArcadeData/arcadedb/issues/6204">#6204</a>), and <code class="language-plaintext highlighter-rouge">verify</code> could fan out to itself and report <code class="language-plaintext highlighter-rouge">ALL_CONSISTENT</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6221">#6221</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> works on a cluster.</strong> Each per-type repair used to be one unsplittable Raft log entry that aborted at commit on a large database; repairs ship as bounded instalments now (<a href="https://github.com/ArcadeData/arcadedb/issues/6128">#6128</a>).</li>
</ul>

<h3 id="timeseries">TimeSeries</h3>

<ul>
  <li><strong>Bucketed aggregation silently dropped everything appended since the last compaction</strong>, because it sized its bucket array from the sealed stores only, so any dashboard query over the newest data was wrong (<a href="https://github.com/ArcadeData/arcadedb/issues/6937">#6937</a>).</li>
  <li><strong>A TimeSeries type whose sealed store failed to load disappeared from the schema</strong> and the database opened as if it had never existed, with the next write creating a fresh empty type; it is registered with its engine unavailable and fails loudly by name now (<a href="https://github.com/ArcadeData/arcadedb/issues/6356">#6356</a>).</li>
  <li><strong>Under HA, a shard whose sealed store grew past 48 MB stopped sealing for ever</strong>, so its samples stayed uncompressed in the mutable bucket with no retention or downsampling; an oversized store ships as ordered slices that followers reassemble and verify, raising the ceiling from 48 MB to roughly 2 GB (<a href="https://github.com/ArcadeData/arcadedb/issues/4416">#4416</a>).</li>
  <li><strong>PromQL fixes</strong>: <code class="language-plaintext highlighter-rouge">or</code> returned <code class="language-plaintext highlighter-rouge">NaN</code> whenever both sides shared a label set, label matchers on an absent column matched backwards, range points were not step-aligned (<a href="https://github.com/ArcadeData/arcadedb/issues/6938">#6938</a>), and <code class="language-plaintext highlighter-rouge">min_over_time</code>/<code class="language-plaintext highlighter-rouge">max_over_time</code> returned <code class="language-plaintext highlighter-rouge">±Infinity</code> for an all-NaN window (<a href="https://github.com/ArcadeData/arcadedb/issues/7039">#7039</a>).</li>
</ul>

<h3 id="wire-protocols">Wire Protocols</h3>

<ul>
  <li><strong>MongoDB.</strong> <code class="language-plaintext highlighter-rouge">findOne</code>/<code class="language-plaintext highlighter-rouge">updateOne</code>/<code class="language-plaintext highlighter-rouge">deleteOne</code> by ObjectId <code class="language-plaintext highlighter-rouge">_id</code> <strong>never matched</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/6745">#6745</a>); <code class="language-plaintext highlighter-rouge">skip</code> and <code class="language-plaintext highlighter-rouge">sort</code> were silently ignored (<a href="https://github.com/ArcadeData/arcadedb/issues/6746">#6746</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6747">#6747</a>); <code class="language-plaintext highlighter-rouge">$exists: false</code> returned the documents that <em>had</em> the field (<a href="https://github.com/ArcadeData/arcadedb/issues/6748">#6748</a>); an upsert filtered on <code class="language-plaintext highlighter-rouge">_id</code> discarded it and created a duplicate on every call (<a href="https://github.com/ArcadeData/arcadedb/issues/6940">#6940</a>); and <code class="language-plaintext highlighter-rouge">{field: null}</code> matched nothing (<a href="https://github.com/ArcadeData/arcadedb/issues/6952">#6952</a>).</li>
  <li><strong>Bolt.</strong> A second <code class="language-plaintext highlighter-rouge">RUN</code> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6804">#6804</a>); a property declared <code class="language-plaintext highlighter-rouge">ARRAY_OF_FLOATS</code> read back as <code class="language-plaintext highlighter-rouge">[F@294b13ce</code> instead of a list, so any client reading embeddings over Bolt got a corrupted string (<a href="https://github.com/ArcadeData/arcadedb/issues/7056">#7056</a>); and a fragmented WebSocket message had its continuation frames discarded (<a href="https://github.com/ArcadeData/arcadedb/issues/6802">#6802</a>).</li>
  <li><strong>gRPC.</strong> <code class="language-plaintext highlighter-rouge">insertStream</code> and <code class="language-plaintext highlighter-rouge">bulkInsert</code> ignored the caller’s <code class="language-plaintext highlighter-rouge">TransactionContext</code> and committed on their own, so rows <strong>survived a subsequent rollback</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/6607">#6607</a>); errors were flattened to <code class="language-plaintext highlighter-rouge">success=false</code> so the client lost the exception type and never retried a conflict (<a href="https://github.com/ArcadeData/arcadedb/issues/6192">#6192</a>); and a stream longer than <code class="language-plaintext highlighter-rouge">txMaxIdleMs</code> was reaped mid-stream and its rows lost (<a href="https://github.com/ArcadeData/arcadedb/issues/6755">#6755</a>).</li>
  <li><strong>GraphQL.</strong> Variables were accepted by the parser but <strong>always resolved to null</strong> and interpolated into the generated SQL as the literal <code class="language-plaintext highlighter-rouge">null</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6834">#6834</a>); the standard <code class="language-plaintext highlighter-rouge">query($a: String, $b: Int)</code> failed to parse because the comma was a real token (<a href="https://github.com/ArcadeData/arcadedb/issues/6860">#6860</a>); and field aliases threw an NPE (<a href="https://github.com/ArcadeData/arcadedb/issues/6384">#6384</a>).</li>
  <li><strong>Redis.</strong> Bulk strings were read byte-by-byte as <code class="language-plaintext highlighter-rouge">(char) b</code>, mangling any non-ASCII payload (<a href="https://github.com/ArcadeData/arcadedb/issues/5907">#5907</a>); a RESP2 null bulk string consumed two extra wire bytes and desynced the connection (<a href="https://github.com/ArcadeData/arcadedb/issues/5911">#5911</a>); and <code class="language-plaintext highlighter-rouge">SET</code> ignored all its options (<a href="https://github.com/ArcadeData/arcadedb/issues/6466">#6466</a>).</li>
  <li><strong>Gremlin.</strong> <code class="language-plaintext highlighter-rouge">ArcadeGraph.close()</code> <strong>committed</strong> 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 (<a href="https://github.com/ArcadeData/arcadedb/issues/6820">#6820</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6821">#6821</a>). The <code class="language-plaintext highlighter-rouge">arcadedb-gremlin</code> coordinate was unusable standalone in 26.8.1 (<a href="https://github.com/ArcadeData/arcadedb/issues/5879">#5879</a>).</li>
</ul>

<h3 id="server-http-and-console">Server, HTTP and Console</h3>

<ul>
  <li><strong>A single HTTP response is bounded by a ceiling no caller can widen.</strong> <code class="language-plaintext highlighter-rouge">httpQueryDefaultLimit</code> protected only callers that stated no limit, so <code class="language-plaintext highlighter-rouge">LIMIT 100000000</code> or <code class="language-plaintext highlighter-rouge">"limit": -1</code> made the server serialise an unbounded result into one JSON response; the new <code class="language-plaintext highlighter-rouge">arcadedb.server.httpQueryMaxResultRows</code> (default 1,000,000) refuses with HTTP 413 rather than truncating (<a href="https://github.com/ArcadeData/arcadedb/issues/5719">#5719</a>).</li>
  <li><strong>The remote client’s watchdog fired after 8h20m instead of 30s</strong>, multiplying the millisecond socket timeout by 1000 (<a href="https://github.com/ArcadeData/arcadedb/issues/5847">#5847</a>), and <code class="language-plaintext highlighter-rouge">RemoteGraphBatch.flush()</code> left the payload buffered on failure so <code class="language-plaintext highlighter-rouge">close()</code> re-sent it and duplicated committed records (<a href="https://github.com/ArcadeData/arcadedb/issues/7031">#7031</a>).</li>
  <li><strong>The <code class="language-plaintext highlighter-rouge">@props</code> type hint leaked into every response.</strong> It appeared in HTTP JSON results for non-element rows, in <code class="language-plaintext highlighter-rouge">toJSON(true)</code> and in WebSocket change events broadcast to every subscriber; it is opt-in now through a <code class="language-plaintext highlighter-rouge">typeHints</code> request flag, which the Java driver sets automatically (<a href="https://github.com/ArcadeData/arcadedb/issues/5812">#5812</a>).</li>
  <li><strong>The console dropped every unescaped backslash</strong> before the command reached the engine, so a Windows path or a regex literal could not be typed, passed with <code class="language-plaintext highlighter-rouge">-b</code>, or replayed with <code class="language-plaintext highlighter-rouge">load</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/6827">#6827</a>); and <code class="language-plaintext highlighter-rouge">connect remote:</code> failed on a password containing a space (<a href="https://github.com/ArcadeData/arcadedb/issues/6830">#6830</a>).</li>
  <li><strong>Kubernetes and Docker quickstarts that could not work.</strong> The StatefulSet example used <code class="language-plaintext highlighter-rouge">${VAR}</code> in <code class="language-plaintext highlighter-rouge">command:</code>, which is never expanded, so the root password became the literal <code class="language-plaintext highlighter-rouge">${rootPassword}</code> and every pod claimed peer name <code class="language-plaintext highlighter-rouge">${HOSTNAME}</code>, and it wired HA on 2424 while Raft binds 2434 (<a href="https://github.com/ArcadeData/arcadedb/issues/6840">#6840</a>). The Docker image pinned <code class="language-plaintext highlighter-rouge">-Xms2G -Xmx2G</code>, so <code class="language-plaintext highlighter-rouge">docker run -m 512m</code> died at startup (<a href="https://github.com/ArcadeData/arcadedb/issues/6841">#6841</a>).</li>
</ul>

<h2 id="upgrade-checklist">Upgrade Checklist</h2>

<p>No schema migration is required and no existing database is rewritten, but this release contains behaviour changes. Five minutes of checking before you upgrade:</p>

<ol>
  <li><strong>Back up first</strong> (it is 27.6x faster now), then read the <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.9.1">breaking changes</a> in full.</li>
  <li><strong>Rolling HA upgrade: upgrade followers before, or together with, the leader.</strong> A node running an older build cannot install a sliced TimeSeries sealed store (<a href="https://github.com/ArcadeData/arcadedb/issues/4416">#4416</a>).</li>
  <li><strong>Grep your schema scripts for out-of-range integral literals.</strong> <code class="language-plaintext highlighter-rouge">SET n = 3000000000</code> on an <code class="language-plaintext highlighter-rouge">INTEGER</code> property now raises a validation error where it used to store <code class="language-plaintext highlighter-rouge">-1294967296</code>. A bulk import carrying such values will surface the error; use the new <code class="language-plaintext highlighter-rouge">-onRowError skip</code> to continue past them.</li>
  <li><strong>Check any HTTP caller relying on <code class="language-plaintext highlighter-rouge">limit: -1</code> or a huge <code class="language-plaintext highlighter-rouge">LIMIT</code></strong> to fetch more than 1,000,000 rows in one response: it now gets HTTP 413. Raise <code class="language-plaintext highlighter-rouge">arcadedb.server.httpQueryMaxResultRows</code> or set it to <code class="language-plaintext highlighter-rouge">-1</code>.</li>
  <li><strong>Check any client reading <code class="language-plaintext highlighter-rouge">@props</code></strong> out of HTTP JSON, <code class="language-plaintext highlighter-rouge">toJSON(true)</code> or WebSocket change events: it is opt-in through the <code class="language-plaintext highlighter-rouge">typeHints</code> request flag now.</li>
  <li><strong>After the upgrade, rebuild two things.</strong> <code class="language-plaintext highlighter-rouge">LSM_TREE</code> indexes written before the #5321 comparator change should be rebuilt; the condition is reported once per logical index as a queryable upgrade warning, visible through <code class="language-plaintext highlighter-rouge">schema:indexes</code> and Studio, naming the <code class="language-plaintext highlighter-rouge">REBUILD INDEX</code> to run (<a href="https://github.com/ArcadeData/arcadedb/issues/5802">#5802</a>). Run <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> once to sweep the orphaned chunk slots this release learned to reclaim.</li>
  <li><strong>If archive size matters more than backup time</strong>, set <code class="language-plaintext highlighter-rouge">arcadedb.backup.compressionLevel=9</code> to keep the old ratio.</li>
  <li><strong>On openCypher</strong>, note that “no labels” is a reserved sentinel type <code class="language-plaintext highlighter-rouge">~NO_LABEL~</code>: <code class="language-plaintext highlighter-rouge">V</code> and <code class="language-plaintext highlighter-rouge">Vertex</code> are ordinary labels now, so <code class="language-plaintext highlighter-rouge">labels(n)</code> on a vertex whose only label was <code class="language-plaintext highlighter-rouge">V</code> changes on pre-26.9.1 data (<a href="https://github.com/ArcadeData/arcadedb/issues/6395">#6395</a>).</li>
</ol>

<h2 id="dependency-updates">Dependency Updates</h2>

<p>Around 120 dependency bumps landed in this cycle, almost all through Dependabot. The notable ones: the Gremlin shaded jar rebuilt on the project-wide <strong>Jackson 2.22.2</strong> so it no longer bundles TinkerPop’s relocated Jackson 2.15.2, Ratis <strong>3.3.0</strong>, Netty <strong>4.2.17.Final</strong>, Undertow <strong>2.4.3.Final</strong>, Lucene <strong>10.5.1</strong>, protobuf-java <strong>4.36.0</strong>, Logback <strong>1.6.3</strong>, snakeyaml <strong>2.7</strong>, JLine <strong>4.4.0</strong>, Micrometer <strong>1.17.1</strong>, OpenTelemetry <strong>1.65.0</strong>, Jedis <strong>8.0.1</strong> and the Neo4j Java driver <strong>6.2.1</strong>. The Gremlin ANTLR runtime and the Groovy major remain deliberately <strong>frozen</strong>, as TinkerPop cannot take a newer one.</p>

<h2 id="getting-started-with-2691">Getting Started with 26.9.1</h2>

<h3 id="docker">Docker</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull arcadedata/arcadedb:26.9.1
</code></pre></div></div>

<p>Visit our <a href="https://hub.docker.com/r/arcadedata/arcadedb">Docker Hub repository</a> for more information.</p>

<h3 id="maven">Maven</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>com.arcadedb<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>arcadedb-engine<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;version&gt;</span>26.9.1<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>All artifacts are available on <a href="https://repo.maven.apache.org/maven2/com/arcadedb/">Maven Central</a>.</p>

<h3 id="documentation">Documentation</h3>

<p>For details on features and usage, see the <a href="https://docs.arcadedb.com/">documentation</a>.</p>

<h2 id="compatibility-note">Compatibility Note</h2>

<p>This release requires no schema migration and rewrites no existing database, so no export or import is needed when upgrading. It does contain behaviour changes, collected in the upgrade checklist above and in full in the <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.9.1">release notes</a>. As always, we recommend creating a database backup before upgrading.</p>

<hr />

<p><strong>Download ArcadeDB 26.9.1 now</strong>: <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.9.1">GitHub Releases</a></p>

<p>Thanks to everyone who reported, reproduced, reviewed, tested and fixed, in particular <a href="https://github.com/232-323">@232-323</a>, <a href="https://github.com/7487">@7487</a>, <a href="https://github.com/ajinsads">@ajinsads</a>, <a href="https://github.com/altugsogutoglu">@altugsogutoglu</a>, <a href="https://github.com/baeseungwon1010">@baeseungwon1010</a>, <a href="https://github.com/borutjures">@borutjures</a>, <a href="https://github.com/cakeni">@cakeni</a>, <a href="https://github.com/chow8386">@chow8386</a>, <a href="https://github.com/danieljuhl">@danieljuhl</a>, <a href="https://github.com/dmoree">@dmoree</a>, <a href="https://github.com/EQSTLab">@EQSTLab</a>, <a href="https://github.com/FEARIS2">@FEARIS2</a>, <a href="https://github.com/g33kroid">@g33kroid</a>, <a href="https://github.com/gramian">@gramian</a>, <a href="https://github.com/GYWang1983">@GYWang1983</a>, <a href="https://github.com/ivan-velikanov">@ivan-velikanov</a>, <a href="https://github.com/jjj-n">@jjj-n</a>, <a href="https://github.com/josh1e">@josh1e</a>, <a href="https://github.com/justinblethrow-cloud">@justinblethrow-cloud</a>, <a href="https://github.com/kl-demi">@kl-demi</a>, <a href="https://github.com/leanworld7-netizen">@leanworld7-netizen</a>, <a href="https://github.com/LepsyMikolaj3301">@LepsyMikolaj3301</a>, <a href="https://github.com/lohithsamaga">@lohithsamaga</a>, <a href="https://github.com/manus-use">@manus-use</a>, <a href="https://github.com/mdre">@mdre</a>, <a href="https://github.com/NooriUta">@NooriUta</a>, <a href="https://github.com/odysseaspenta">@odysseaspenta</a>, <a href="https://github.com/ruispereira">@ruispereira</a>, <a href="https://github.com/ruslan-butyk-fntext">@ruslan-butyk-fntext</a>, <a href="https://github.com/sbsrouteur">@sbsrouteur</a>, <a href="https://github.com/shulei5831sl">@shulei5831sl</a>, <a href="https://github.com/syntact-io-office-user">@syntact-io-office-user</a>, <a href="https://github.com/T4ran24">@T4ran24</a>, <a href="https://github.com/tae898">@tae898</a>, <a href="https://github.com/tobiasdam">@tobiasdam</a>, <a href="https://github.com/TobiasJoseHermann">@TobiasJoseHermann</a>, <a href="https://github.com/tonghuaroot">@tonghuaroot</a>, <a href="https://github.com/waterWang">@waterWang</a>, <a href="https://github.com/YGY-001">@YGY-001</a> and <a href="https://github.com/ZwaarContrast">@ZwaarContrast</a>.</p>

<p>Luca Garulli
ArcadeDB Founder</p>]]></content><author><name>Luca Garulli</name></author><category term="Multi-Model" /><category term="Backup" /><category term="Vector Search" /><category term="OpenCypher" /><category term="PostgreSQL" /><category term="Security" /><category term="Graph Database" /><category term="Release" /><summary type="html"><![CDATA[ArcadeDB 26.9.1 is the largest release we have ever shipped: 992 issues and pull requests closed, 1,500 commits. Full backups are 27.6x faster and no longer stall writers, a family of wrong-result defects in SQL and openCypher is closed, indexes are used where they were not, vector search opens in constant time, and six security advisories are patched.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/release-v26.9.1.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/release-v26.9.1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB 26.8.1: Concurrent Writes Without Conflicts, Safe Vertex Deletes, and 13 Security Advisories</title><link href="https://arcadedb.com/blog/arcadedb-26-8-1/" rel="alternate" type="text/html" title="ArcadeDB 26.8.1: Concurrent Writes Without Conflicts, Safe Vertex Deletes, and 13 Security Advisories" /><published>2026-08-03T00:00:00+00:00</published><updated>2026-08-03T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-26-8-1</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-26-8-1/"><![CDATA[<p><strong>ArcadeDB 26.8.1</strong> is a major release: <strong>381 issues and pull requests closed</strong> under the <a href="https://github.com/ArcadeData/arcadedb/milestone/60">26.8.1 milestone</a>, 281 issues and 100 PRs, out of 298 pull requests merged and <strong>669 commits</strong> since <a href="https://arcadedb.com/blog/arcadedb-26-7-2/">26.7.2</a>. It also carries everything shipped in the <a href="https://arcadedb.com/blog/arcadedb-26-7-3/">26.7.3</a> hotfix.</p>

<p>The headline work is in four places:</p>

<ul>
  <li><strong>Concurrency</strong>: concurrent writes to unrelated records of the same page no longer conflict, which is what made ArcadeDB usable under real multi-writer load on types with few buckets.</li>
  <li><strong>Graph integrity</strong>: a family of vertex and edge delete defects that could silently lose edges under concurrency is closed, and deleting a super-node vertex got <strong>5x faster</strong> along the way.</li>
  <li><strong>Storage and indexes</strong>: bloom filters on compacted LSM series, a schema dictionary that is no longer capped at one page, a geospatial index that costs one entry per point instead of eleven, and TimeSeries TAG columns that are dictionary-encoded.</li>
  <li><strong>Security</strong>: 13 advisories closed, most of them on the wire protocols and the MCP endpoint.</li>
</ul>

<p><strong>Upgrading is strongly recommended for every deployment.</strong> There are breaking changes and behaviour changes; no schema migration is required, and no existing database is rewritten.</p>

<h2 id="major-highlights">Major Highlights</h2>

<h3 id="concurrent-writes-to-unrelated-records-of-the-same-page-no-longer-conflict">Concurrent Writes to Unrelated Records of the Same Page No Longer Conflict</h3>

<p>ArcadeDB detects write conflicts per <em>page</em>, so two transactions that touched the same bucket page raised a <code class="language-plaintext highlighter-rouge">ConcurrentModificationException</code> even when they wrote completely unrelated records that merely happened to share it. On a type with few buckets and many concurrent writers this made a retry pointless: the retry ran straight into the same collision (<a href="https://github.com/ArcadeData/arcadedb/issues/5279">#5279</a>).</p>

<p>All three halves of that are gone:</p>

<ul>
  <li><strong>Inserts</strong> into one page reserve their slot per in-flight transaction, so concurrent inserts get different positions (and different RIDs) instead of all being handed the same one.</li>
  <li><strong>Updates</strong> are replayed by the commit-time disjoint-slot merge whenever they stayed inside the page, which now includes a record that <strong>grew</strong> (a longer string, one more property) and not only an overwrite of the same size or smaller. Growth is the normal update shape, so leaving it out kept concurrent updates conflicting.</li>
  <li><strong>Deletes</strong> of a plain in-place record are replayed too (<a href="https://github.com/ArcadeData/arcadedb/issues/5569">#5569</a>). Such a delete only zeroes one slot-table entry, so it commutes with writes to every other slot.</li>
</ul>

<p>Measured on the reported workload (one single bucket, <code class="language-plaintext highlighter-rouge">attempts=1</code>, no retry):</p>

<table>
  <thead>
    <tr>
      <th>Scenario</th>
      <th>Before</th>
      <th>After</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Concurrent inserts</td>
      <td>~1750 conflicts / 2000</td>
      <td>0</td>
    </tr>
    <tr>
      <td>Concurrent sub-graph creation (6 vertices + 5 edges per transaction)</td>
      <td>~270 / 320</td>
      <td>0</td>
    </tr>
    <tr>
      <td>10 transactions updating 10 different records of one page</td>
      <td>9 failed / 10</td>
      <td>0</td>
    </tr>
    <tr>
      <td>Sustained updates, 8 writers on their own records of one page</td>
      <td>~2083 / 2880</td>
      <td>0</td>
    </tr>
    <tr>
      <td>8 deletes + 8 updates of 16 different records of one page</td>
      <td>15 failed / 16</td>
      <td>0</td>
    </tr>
    <tr>
      <td>10 transactions deleting 10 different records of one page</td>
      <td>9 failed / 10</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<p>A <code class="language-plaintext highlighter-rouge">ConcurrentModificationException</code> is still raised, by design, when two transactions really write the <strong>same</strong> record: a byte-for-byte pre-image check makes sure no concurrent write is ever silently overwritten. Nothing changes for single-writer workloads and no application change is needed. The merge can be switched off with <code class="language-plaintext highlighter-rouge">arcadedb.txPageSlotMerge=false</code>.</p>

<p>The merges also <strong>prove their coverage</strong> now instead of trusting every writer to declare its pages (<a href="https://github.com/ArcadeData/arcadedb/issues/5596">#5596</a>): a page carrying even one byte written outside a declared, replayable write is refused and falls back to the ordinary retry. The three merge counters (<code class="language-plaintext highlighter-rouge">edgeAppendMerges</code>, <code class="language-plaintext highlighter-rouge">txPageSlotMerges</code>, <code class="language-plaintext highlighter-rouge">mergesDeclinedByCoverage</code>) are now visible to an operator in the <code class="language-plaintext highlighter-rouge">PAGE-MANAGER</code> block, in <code class="language-plaintext highlighter-rouge">/api/v1/server</code>, in Studio and on <code class="language-plaintext highlighter-rouge">/prometheus</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5608">#5608</a>).</p>

<h3 id="deleting-a-vertex-no-longer-loses-its-edges-and-is-up-to-5x-faster">Deleting a Vertex No Longer Loses Its Edges, and Is Up to 5x Faster</h3>

<p>Four defects, all on the same path, all capable of committing a graph with edges pointing at a vertex that is gone (<a href="https://github.com/ArcadeData/arcadedb/issues/5670">#5670</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5680">#5680</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5725">#5725</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5760">#5760</a>):</p>

<ul>
  <li>An edge-list chunk that is <strong>momentarily unreadable</strong>, a normal MVCC window on a hot vertex rather than a fact about the graph, was read as “nothing to remove here”. The removal ended having removed nothing and the edge record was deleted anyway, leaving a back-reference on a neighbour. It is now a retryable <code class="language-plaintext highlighter-rouge">ConcurrentModificationException</code>, so the transaction re-reads a consistent view and completes the removal.</li>
  <li>The same window on the <strong>vertex’s own</strong> list meant <em>no edges collected</em>, and the vertex record deleted on top of that empty view.</li>
  <li>An <strong>edge appended while the delete was running</strong> survived the delete with a live <code class="language-plaintext highlighter-rouge">out</code> and an <code class="language-plaintext highlighter-rouge">in</code> naming a record that no longer exists. A vertex delete now pins every page its edge list can grow through, at the version it read the list at.</li>
  <li>Each edge was disconnected from <strong>both</strong> endpoints, one of which is always the vertex being deleted, whose lists are dropped wholesale moments later. Each edge is now disconnected from its far end only.</li>
</ul>

<p>That last one is also where the performance is. 100k edges into one hub, on an Apple M-series laptop:</p>

<table>
  <thead>
    <tr>
      <th>Layout</th>
      <th>Before</th>
      <th>After</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Promoted super-node (striped)</td>
      <td>2374 ms</td>
      <td>446 ms</td>
    </tr>
    <tr>
      <td>Classic single chain</td>
      <td>350 ms</td>
      <td>308 ms</td>
    </tr>
  </tbody>
</table>

<p>The removal walk <strong>streams</strong> now: <code class="language-plaintext highlighter-rouge">deleteVertex</code> no longer materialises every edge into a list first, so there is no per-degree allocation on the path at all (tens of megabytes of retained heap on a million-edge super-node).</p>

<blockquote>
  <p><strong>Visible effect.</strong> <code class="language-plaintext highlighter-rouge">vertex.delete()</code> / <code class="language-plaintext highlighter-rouge">DELETE VERTEX</code> and <code class="language-plaintext highlighter-rouge">edge.delete()</code> / <code class="language-plaintext highlighter-rouge">DELETE EDGE</code> can now raise a retryable <code class="language-plaintext highlighter-rouge">ConcurrentModificationException</code> where they previously “succeeded” while losing an edge. It is a <code class="language-plaintext highlighter-rouge">NeedRetryException</code>, so <code class="language-plaintext highlighter-rouge">database.transaction(...)</code> and the server’s auto-retry for single-request commands absorb it. A client-managed explicit transaction over <code class="language-plaintext highlighter-rouge">RemoteDatabase</code> spans several HTTP requests, so its commit is not auto-retried and the caller should retry the transaction. A delete that keeps failing however often it is retried means the list is genuinely broken: the error now names the repair, <code class="language-plaintext highlighter-rouge">CHECK DATABASE RECORD #12:3 FIX</code>, and the retry after it goes through.</p>
</blockquote>

<h3 id="bloom-filters-on-compacted-lsm-indexes-enabled-by-default">Bloom Filters on Compacted LSM Indexes (Enabled by Default)</h3>

<p>An LSM index lookup walks every compacted series from newest to oldest, and a series whose key <em>range</em> covers the key still costs a root-page search and a data-page read to discover it does not hold it. Each compacted series now carries a bloom filter that answers from a single 8 KB page (<a href="https://github.com/ArcadeData/arcadedb/issues/5517">#5517</a>).</p>

<p>Measured on 2M keys across 9 series (<code class="language-plaintext highlighter-rouge">LSMTreeBloomFilterBenchmark</code>):</p>

<table>
  <thead>
    <tr>
      <th>Measurement</th>
      <th>Filters off</th>
      <th>Filters on</th>
      <th>Gain</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Absent-key lookups (a duplicate check)</td>
      <td>199,743/s</td>
      <td>386,138/s</td>
      <td><strong>1.9x</strong></td>
    </tr>
    <tr>
      <td>Pages read for those lookups</td>
      <td>1,490</td>
      <td>705</td>
      <td><strong>2.1x fewer</strong></td>
    </tr>
    <tr>
      <td>Bytes read for those lookups</td>
      <td>372 MB</td>
      <td>176 MB</td>
      <td>2.1x fewer</td>
    </tr>
    <tr>
      <td>Present-key lookups</td>
      <td>244,000/s</td>
      <td>281,150/s</td>
      <td>1.2x</td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>On by default</strong> at a 1% target false-positive rate: <code class="language-plaintext highlighter-rouge">arcadedb.indexBloomFilterRate=0.01</code>. Set <code class="language-plaintext highlighter-rouge">0</code> to disable.</li>
  <li><strong>~1.2 bytes per key</strong> on disk, about 3% of the index it describes, in a <code class="language-plaintext highlighter-rouge">&lt;index&gt;_bf.bfidx</code> component.</li>
  <li><strong>No rebuild and no migration.</strong> Filters are written by compaction, so an existing index gains them at its next compaction. They replicate over HA and are included in backups.</li>
  <li><strong>Helps most when compacted series overlap in key range</strong>: an email, a UUID, a business id. Ascending keys give each series a disjoint slice the root page already rules out. Range scans never consult them.</li>
  <li><strong>Observability</strong>: <code class="language-plaintext highlighter-rouge">bloomSkippedSeries</code> and <code class="language-plaintext highlighter-rouge">bloomProbedSeries</code> in the index statistics.</li>
</ul>

<p>Backward and forward compatible with no version bump: an older ArcadeDB does not recognise the <code class="language-plaintext highlighter-rouge">.bfidx</code> extension and reads the index exactly as it does today.</p>

<h3 id="the-schema-dictionary-is-no-longer-capped-at-a-single-page">The Schema Dictionary Is No Longer Capped at a Single Page</h3>

<p>Every type and property name is mapped to a small integer id, and that table lived in <strong>one page</strong>: 48,396 short names measured. Past it, <code class="language-plaintext highlighter-rouge">CREATE PROPERTY</code> and inserting a document with a new field name failed permanently with <code class="language-plaintext highlighter-rouge">No space left in dictionary file</code>, with no way to grow and no way back (<a href="https://github.com/ArcadeData/arcadedb/pull/5560">#5560</a>).</p>

<p>Names now roll over onto further pages, so the cap is gone.</p>

<ul>
  <li><strong>No migration, and existing databases are not rewritten.</strong> A dictionary written by an earlier version <em>is</em> a dictionary of one page and loads unchanged; it gains rollover on the next write that needs it.</li>
  <li><strong>Appending a name is no longer quadratic.</strong> Growing to 500,000 names took <strong>11.2s</strong> of pure array copying; it now takes <strong>2ms</strong>.</li>
  <li><strong>New databases use a 65,536-byte dictionary page</strong> instead of 327,680, so a new name dirties and flushes 5x less. Existing databases keep the page size they were created with.</li>
</ul>

<blockquote>
  <p><strong>Rolling upgrade: upgrade followers before, or together with, the leader.</strong> Dictionary pages replicate as raw pages, and a follower on an older build writes page 1+ but reloads only page 0. A database that has rolled over can no longer be opened by an older ArcadeDB at all, loudly rather than silently.</p>
</blockquote>

<h3 id="geospatial-index-a-point-costs-one-entry-instead-of-eleven">Geospatial Index: a Point Costs One Entry Instead of Eleven</h3>

<p>A <code class="language-plaintext highlighter-rouge">GEOSPATIAL</code> index stored the whole GeoHash ancestor chain, so a single point wrote one entry per tree level, 11 by default. Everything an index write costs was multiplied by 11, and the continent-sized cells at the top of the tree collected one posting per record and grew without bound, which is why a bulk load with a geospatial index got slower the longer it ran and finally failed with <code class="language-plaintext highlighter-rouge">ReplicatedEntryTooLargeException</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5478">#5478</a>).</p>

<p>On an LSM-Tree, “every cell below C” is simply a key range, so the ancestors do not need to be materialised. The index now stores only the frontier cells, <strong>exactly one</strong> for a point, and answers a query with a prefix range scan. On a 1M-point load into one country-sized box (<code class="language-plaintext highlighter-rouge">GeoIndexIngestBenchmark</code>):</p>

<table>
  <thead>
    <tr>
      <th>Arm</th>
      <th>Wall clock</th>
      <th>Index entries</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>No index (the floor)</td>
      <td>10.8 s</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td>New layout</td>
      <td>13.0 s</td>
      <td>1,000,000</td>
    </tr>
    <tr>
      <td>Old layout</td>
      <td>22.4 s</td>
      <td>11,000,000</td>
    </tr>
  </tbody>
</table>

<p>The index costs <strong>5.3x less</strong>, the whole load is <strong>1.7x faster</strong>, and the index is <strong>11x smaller</strong> on disk. Area shapes index fewer cells still: a complete set of sibling cells collapses into its parent, 57% fewer for a small square and 74% for a jagged outline. Queries are also more selective and stream their candidates instead of materialising the whole set, and a <code class="language-plaintext highlighter-rouge">POINT</code> search shape now uses the index at all (<code class="language-plaintext highlighter-rouge">geo.equals</code> / <code class="language-plaintext highlighter-rouge">geo.contains</code> used to find nothing and fall back to a full scan).</p>

<blockquote>
  <p>Existing indexes keep working and are <strong>not</strong> rewritten: the layout is recorded per index. Opening the database says so once per index, and Studio shows a banner with the ready-to-run statement: <code class="language-plaintext highlighter-rouge">REBUILD INDEX `Address[location]`</code>. Note the <code class="language-plaintext highlighter-rouge">shapeRel</code> half of the fix lives in the shared query walk, so an index still on the old layout also stops skipping covering cells the moment the jar is swapped.</p>
</blockquote>

<h3 id="timeseries-tag-columns-are-dictionary-encoded-36x-less-page-traffic">TimeSeries TAG Columns Are Dictionary-Encoded: 36x Less Page Traffic</h3>

<p>A TimeSeries mutable row is fixed-stride, so a <code class="language-plaintext highlighter-rouge">STRING</code> TAG column reserved 258 bytes whether the tag was <code class="language-plaintext highlighter-rouge">us-east-1</code> or empty. Tags are low-cardinality by definition, so nearly all of that was padding that still had to be written, flushed and shipped through the WAL (<a href="https://github.com/ArcadeData/arcadedb/issues/5519">#5519</a>).</p>

<p>A TAG column now holds a 4-byte id into a per-type append-only dictionary component:</p>

<table>
  <thead>
    <tr>
      <th>Arm</th>
      <th>Stride</th>
      <th>Rows per 64K page</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1 tag, 3 fields</td>
      <td>290 B to <strong>36 B</strong></td>
      <td>225 to <strong>1819</strong></td>
    </tr>
    <tr>
      <td>10 tags, 3 fields</td>
      <td>2612 B to <strong>72 B</strong></td>
      <td>25 to <strong>909</strong></td>
    </tr>
    <tr>
      <td>10 tags, 10 fields</td>
      <td>2668 B to <strong>128 B</strong></td>
      <td>24 to <strong>511</strong></td>
    </tr>
  </tbody>
</table>

<p>The ten-tag arm went from writing 50.0 MB of pages for 2.1 MB of payload (23x amplification) to 1.4 MB (0.7x), and from 29.9 ms to 6.0 ms. Corroborated independently on real TSBS data (2,592,000 points) by <a href="https://github.com/tae898">@tae898</a> in the issue.</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">arcadedb.timeSeriesTagDictionaryMaxSize</code></strong> caps distinct values per type, default 1M.</li>
  <li><strong>STRING <em>fields</em> stay inline</strong>: a field is where high-cardinality text belongs.</li>
  <li><strong>Existing types keep the inline layout.</strong> The row format is versioned per type; a new TimeSeries type gets the encoding, an existing one has to be recreated to gain it. If you are benchmarking, point the harness at a fresh database or you will measure the old layout.</li>
</ul>

<h3 id="mcp-a-module-of-its-own-new-tools-and-per-principal-scoping">MCP: a Module of Its Own, New Tools, and Per-Principal Scoping</h3>

<p>The MCP server is now a dedicated <code class="language-plaintext highlighter-rouge">arcadedb-mcp</code> module (<a href="https://github.com/ArcadeData/arcadedb/issues/5692">#5692</a>) and gained the tool surface that makes it usable as a GraphRAG back-end:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">vector_search</code></strong> (dense and sparse), <strong><code class="language-plaintext highlighter-rouge">hybrid_search</code></strong>, <strong><code class="language-plaintext highlighter-rouge">full_text_search</code></strong> and a bounded <strong><code class="language-plaintext highlighter-rouge">sample_records</code></strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/4860">#4860</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/4861">#4861</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/4862">#4862</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/4863">#4863</a>).</li>
  <li><strong>Prompts</strong> <code class="language-plaintext highlighter-rouge">graphrag_query</code> and <code class="language-plaintext highlighter-rouge">build_knowledge_graph</code>, the latter with an enforceable source-text fence (<a href="https://github.com/ArcadeData/arcadedb/issues/4866">#4866</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5586">#5586</a>).</li>
  <li><strong>Configurable tool profiles</strong>, <strong>per-database permission scoping</strong> and <strong>per-principal profiles on a shared endpoint</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/4867">#4867</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/4868">#4868</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5445">#5445</a>).</li>
  <li><strong>MCP 2025-03-26 transport conformance</strong>: batches, notifications, GET and Origin handling (<a href="https://github.com/ArcadeData/arcadedb/issues/5394">#5394</a>), plus proper JSON-RPC <code class="language-plaintext highlighter-rouge">-32602</code> for malformed members instead of HTTP 500 (<a href="https://github.com/ArcadeData/arcadedb/issues/5585">#5585</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5620">#5620</a>).</li>
</ul>

<p>Much of this arrived from <a href="https://github.com/justinblethrow-cloud">@justinblethrow-cloud</a>.</p>

<h3 id="experimental-graalvm-native-image-build-of-the-server">Experimental: GraalVM Native-Image Build of the Server</h3>

<p>An experimental native-image build of the ArcadeDB server is now part of the build (<a href="https://github.com/ArcadeData/arcadedb/issues/5544">#5544</a>): a single self-contained binary with no JVM start-up cost. Experimental means exactly that, it is not yet part of the published distribution.</p>

<h2 id="security-advisories">Security Advisories</h2>

<p>This release closes <strong>13 security advisories</strong>, on top of the three closed in 26.7.3. Each is published in full (impact, affected versions and credit) as a <a href="https://github.com/ArcadeData/arcadedb/security/advisories">GitHub Security Advisory</a> on the repository; the summaries below are only a map of what changed. <strong>Upgrading is strongly recommended for any deployment that exposes a wire protocol, the MCP endpoint, or accepts queries from untrusted callers.</strong></p>

<p><strong>Authentication and authorization on the wire protocols</strong></p>

<ul>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-fq9c-x968-g278">GHSA-fq9c-x968-g278</a></strong>: the MongoDB protocol accepted commands without authenticating the caller. It now authenticates and enforces per-database authorization.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-m46c-jh3x-xwrp">GHSA-m46c-jh3x-xwrp</a></strong>: the Redis protocol required no authentication. It now requires <code class="language-plaintext highlighter-rouge">AUTH</code>, supports the <code class="language-plaintext highlighter-rouge">HELLO</code> handshake, and can be served over TLS.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-c287-v325-j5jx">GHSA-c287-v325-j5jx</a></strong>: the Gremlin protocol did not enforce per-database and per-type authorization.</li>
</ul>

<p><strong>The authenticated principal is now bound on every execution path</strong></p>

<p>The engine’s per-user permission gates are deliberately no-ops when no principal is bound on the thread, which is how embedded and replication contexts skip them. Three paths reached the engine without binding it, so every gate silently passed:</p>

<ul>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-p29f-345w-4qwf">GHSA-p29f-345w-4qwf</a></strong>: the gRPC transaction thread.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-5j4x-3jfw-8xv3">GHSA-5j4x-3jfw-8xv3</a></strong>: the async command worker thread.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-c23x-pqcj-7hfm">GHSA-c23x-pqcj-7hfm</a></strong>: the batch and time-series HTTP handlers, so per-type ACLs did not enforce.</li>
</ul>

<p><strong>Privileged operations that were not gated</strong></p>

<ul>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-pff6-hp53-pj54">GHSA-pff6-hp53-pj54</a></strong>: the server-administration MCP tools (<code class="language-plaintext highlighter-rouge">set_server_setting</code> and the profiler controls) gated only on the global <code class="language-plaintext highlighter-rouge">allowAdmin</code> flag and ignored the caller. They are root-only now, matching <code class="language-plaintext highlighter-rouge">POST /api/v1/server</code>.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-vv82-qvpf-rjwv">GHSA-vv82-qvpf-rjwv</a></strong>: <code class="language-plaintext highlighter-rouge">DELETE FUNCTION</code> did not require <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code>.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-hfp5-6gcp-8c75">GHSA-hfp5-6gcp-8c75</a></strong>: Cypher <code class="language-plaintext highlighter-rouge">LOAD CSV</code> could read local files without administrative privilege.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-qwgr-2c45-63xx">GHSA-qwgr-2c45-63xx</a></strong>: the database name was not validated when creating or dropping a database, allowing path traversal outside the configured database directory.</li>
</ul>

<p><strong>Untrusted input reaching the host</strong></p>

<ul>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-4w2m-77c8-83mw">GHSA-4w2m-77c8-83mw</a></strong>: a caller-supplied URL was validated only on its first hop and re-resolved after the check, so a redirect or a DNS rebind could reach an address the validation had rejected (SSRF). Every hop is validated now, and the validated address is pinned for the duration of the fetch.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-wx28-2265-f788">GHSA-wx28-2265-f788</a></strong>: the scripting host-class allow-list was matched as a regular expression, so an entry could admit far more classes than it names. It is matched literally now.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-xmjm-8q85-g778">GHSA-xmjm-8q85-g778</a></strong>: <code class="language-plaintext highlighter-rouge">range()</code> materialised every element, so one query could exhaust the heap. The list is lazy now and a range beyond <code class="language-plaintext highlighter-rouge">arcadedb.queryMaxRangeSize</code> is refused as a client error.</li>
</ul>

<p><strong>Also hardened in this release</strong></p>

<ul>
  <li><strong>MongoDB protocol: field names and filter values can no longer inject SQL.</strong> A MongoDB command is translated into SQL, and the field names and values taken off the wire were embedded without escaping. A filter value containing a single quote closed the string literal, so an <code class="language-plaintext highlighter-rouge">updateMany</code> with a crafted <code class="language-plaintext highlighter-rouge">name</code> updated <strong>every</strong> document; a <code class="language-plaintext highlighter-rouge">$unset</code> / <code class="language-plaintext highlighter-rouge">$inc</code> field name containing a back-tick removed a property the client never asked for. Values are now bound as parameters and every field name is back-tick quoted, one dot-separated segment at a time (<a href="https://github.com/ArcadeData/arcadedb/issues/5579">#5579</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5583">#5583</a>).</li>
  <li><strong>Server and cluster status endpoints scope their per-database output to the caller.</strong> <code class="language-plaintext highlighter-rouge">GET /api/v1/cluster</code>, the <code class="language-plaintext highlighter-rouge">ha.databases</code> array of <code class="language-plaintext highlighter-rouge">GET /api/v1/server?mode=cluster</code> and the <code class="language-plaintext highlighter-rouge">metrics.sparseVectorIndexes</code> map now reduce every per-database entry to the databases the caller is authorized for. <code class="language-plaintext highlighter-rouge">POST /api/v1/cluster/bootstrap-state</code> and <code class="language-plaintext highlighter-rouge">GET /api/v1/cluster?presence=true</code> move behind the root check the seven mutating Raft endpoints already use.</li>
  <li><strong>Studio no longer carries schema names in inline <code class="language-plaintext highlighter-rouge">onclick</code> handlers</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5580">#5580</a>).</li>
</ul>

<h2 id="new-features">New Features</h2>

<h3 id="storage-and-indexes">Storage and Indexes</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">CHECK DATABASE RECORD &lt;rid&gt;</code></strong>: check and repair named records only, instead of two full passes over a type (<a href="https://github.com/ArcadeData/arcadedb/issues/5680">#5680</a>). Combines with <code class="language-plaintext highlighter-rouge">FIX</code> and <code class="language-plaintext highlighter-rouge">COMPRESS</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> reclaims orphaned edge-list segments</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5375">#5375</a>), and rebuilds unreadable edge lists from the surviving edge records.</li>
  <li><strong>Live progress</strong> for <code class="language-plaintext highlighter-rouge">CHECK DATABASE</code>, <code class="language-plaintext highlighter-rouge">REBUILD INDEX</code>, <code class="language-plaintext highlighter-rouge">COMPACT INDEX</code>, <code class="language-plaintext highlighter-rouge">BACKUP DATABASE</code> and <code class="language-plaintext highlighter-rouge">IMPORT DATABASE</code>, in the engine, over HTTP, in the console and in Studio (<a href="https://github.com/ArcadeData/arcadedb/issues/5372">#5372</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5376">#5376</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">COMPACT INDEX</code></strong> is reachable from SQL/HTTP, not only from the Java API (<a href="https://github.com/ArcadeData/arcadedb/issues/5144">#5144</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">GEOSPATIAL</code> indexes have a builder of their own</strong>, so <code class="language-plaintext highlighter-rouge">precision</code> and <code class="language-plaintext highlighter-rouge">tokenization</code> are settable from SQL: <code class="language-plaintext highlighter-rouge">CREATE INDEX ON Location (coords) GEOSPATIAL METADATA {"precision": 6}</code>.</li>
  <li><strong>An <code class="language-plaintext highlighter-rouge">LSM_VECTOR</code> index compacts itself</strong> once its file is mostly garbage (<a href="https://github.com/ArcadeData/arcadedb/issues/5516">#5516</a>).</li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">HASH</code> indexes can key on a <code class="language-plaintext highlighter-rouge">LINK</code></strong>, so an edge type’s <code class="language-plaintext highlighter-rouge">@out</code>/<code class="language-plaintext highlighter-rouge">@in</code> pair can be indexed <code class="language-plaintext highlighter-rouge">UNIQUE_HASH</code>, the structural way to enforce edge de-duplication, which used to be accepted and then fail on every insert claiming page corruption (<a href="https://github.com/ArcadeData/arcadedb/issues/5677">#5677</a>):</p>

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">INDEX</span> <span class="k">ON</span> <span class="n">INITIATED</span> <span class="p">(</span><span class="nv">`@out`</span><span class="p">,</span> <span class="nv">`@in`</span><span class="p">)</span> <span class="n">UNIQUE_HASH</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="server-and-operations">Server and Operations</h3>

<ul>
  <li><strong>Opt-in SLF4J logging.</strong> <code class="language-plaintext highlighter-rouge">Slf4jLogger</code> routes ArcadeDB’s logs through the SLF4J facade instead of writing to stdout, keeping <code class="language-plaintext highlighter-rouge">java.util.logging</code> as the default, so an embedding host application gets consistent logs (<a href="https://github.com/ArcadeData/arcadedb/issues/4276">#4276</a>). <code class="language-plaintext highlighter-rouge">arcadedb.log.impl</code> is now a proper <code class="language-plaintext highlighter-rouge">GlobalConfiguration</code> entry (<a href="https://github.com/ArcadeData/arcadedb/issues/5543">#5543</a>). Contributed by <a href="https://github.com/ruispereira">@ruispereira</a>.</li>
  <li><strong>The OpenAPI spec matches the registered HTTP route surface</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/4895">#4895</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">arcadedb.server.httpQueryDefaultLimit</code></strong> makes the HTTP row cap configurable (default 20000, <code class="language-plaintext highlighter-rouge">-1</code> for unlimited), and truncated responses say so.</li>
</ul>

<h3 id="query-engines">Query Engines</h3>

<ul>
  <li><strong>Parallel top-K for <code class="language-plaintext highlighter-rouge">LSM_SPARSE_VECTOR</code></strong>: a sparse top-K is split into parallel RID ranges (<a href="https://github.com/ArcadeData/arcadedb/issues/4085">#4085</a>), <strong>3.4x</strong> on real SPLADE data.</li>
  <li><strong>Cypher <code class="language-plaintext highlighter-rouge">CREATE CONSTRAINT ... IS UNIQUE</code> / <code class="language-plaintext highlighter-rouge">IS NODE KEY</code></strong> upgrade a plain index in place, so a Neo4j migration script that creates indexes and then constraints keeps working.</li>
</ul>

<h2 id="major-fixes-and-improvements">Major Fixes and Improvements</h2>

<h3 id="bulk-load">Bulk Load</h3>

<ul>
  <li><strong>A failed bulk load answers immediately instead of waiting for the rest of the upload.</strong> <code class="language-plaintext highlighter-rouge">POST /api/v1/batch</code> rejects a payload it cannot use, but did so only after reading the <em>rest</em> of the upload, so on a 25M-line load the client was told nothing for fifteen minutes and then nothing at all (<code class="language-plaintext highlighter-rouge">UT000002: The response has already been started</code>). The verdict is now delivered as soon as it is reached (<a href="https://github.com/ArcadeData/arcadedb/issues/5470">#5470</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">GraphBatch.close()</code> restores the configured WAL flush</strong>, not the default, so durability is no longer silently downgraded after any bulk load (<a href="https://github.com/ArcadeData/arcadedb/issues/5378">#5378</a>).</li>
  <li><strong>Two concurrent <code class="language-plaintext highlighter-rouge">GraphBatch</code> instances on the same database are refused</strong> instead of silently losing edges (<a href="https://github.com/ArcadeData/arcadedb/issues/5666">#5666</a>).</li>
  <li><strong>Time-series HTTP ingest batches into one append transaction per measurement.</strong></li>
</ul>

<h3 id="graph-engine">Graph Engine</h3>

<ul>
  <li><strong>Deleting an edge no longer leaves its back-reference behind under concurrency</strong>, on all three operations that disconnect one: <code class="language-plaintext highlighter-rouge">DELETE EDGE</code>, moving an edge, and <code class="language-plaintext highlighter-rouge">DELETE VERTEX</code> (see the highlight above).</li>
  <li><strong>Ghost-edge pruning during iteration no longer rolls back and replaces the caller’s transaction</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5694">#5694</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">TX_RETRY_DELAY</code> is read from the database configuration</strong> instead of the global static, so a per-database override applies (<a href="https://github.com/ArcadeData/arcadedb/issues/5693">#5693</a>).</li>
  <li><strong>Broken multi-page records are deletable again</strong> by every path, and <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> repairs them.</li>
</ul>

<h3 id="indexes">Indexes</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">countEntries()</code> no longer counts tombstones as live entries.</strong> Deleting every record of a type left the index reporting <code class="language-plaintext highlighter-rouge">1</code> with zero records in the database (<a href="https://github.com/ArcadeData/arcadedb/issues/5601">#5601</a>).</li>
  <li><strong>An index cursor never hands out a <code class="language-plaintext highlighter-rouge">null</code> entry</strong>, <code class="language-plaintext highlighter-rouge">hasNext()</code> is exact, <code class="language-plaintext highlighter-rouge">next()</code> throws <code class="language-plaintext highlighter-rouge">NoSuchElementException</code> once exhausted, and <code class="language-plaintext highlighter-rouge">getRecord()</code> / <code class="language-plaintext highlighter-rouge">getKeys()</code> describe the entry <code class="language-plaintext highlighter-rouge">next()</code> <strong>last returned</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5635">#5635</a>). Two user-visible consequences fixed with it: <code class="language-plaintext highlighter-rouge">SELECT min(...)</code> / <code class="language-plaintext highlighter-rouge">max(...)</code> could answer with a <strong>deleted</strong> key, and a delete-heavy index reported one entry too many.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Cursor</code> is <code class="language-plaintext highlighter-rouge">AutoCloseable</code></strong>, and a leaked cursor no longer pins a retired index file for the lifetime of the database (<a href="https://github.com/ArcadeData/arcadedb/issues/5662">#5662</a>).</li>
  <li><strong>A <code class="language-plaintext highlighter-rouge">HASH</code> index refuses a page size its bucket pages cannot address.</strong> Above 65536 bytes the 16-bit slot offsets truncate and the index destroys itself on insert, reported as <code class="language-plaintext highlighter-rouge">Detected cycle in hash index</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5713">#5713</a>).</li>
  <li><strong>Partitioned types, three fixes.</strong> A lookup on a <strong>secondary</strong> index was pruned to the bucket the <em>lookup</em> key hashed to rather than the partition key’s, so a secondary <code class="language-plaintext highlighter-rouge">UNIQUE</code> index stopped rejecting duplicates (<a href="https://github.com/ArcadeData/arcadedb/issues/5589">#5589</a>). A lookup key boxed differently than the stored value hashed differently, so on a <code class="language-plaintext highlighter-rouge">LONG</code> partition key every negative value missed (<a href="https://github.com/ArcadeData/arcadedb/issues/5595">#5595</a>). And a partition key whose bucket is not a function of the index key (<code class="language-plaintext highlighter-rouge">BINARY</code>, <code class="language-plaintext highlighter-rouge">DECIMAL</code>, zone-carrying <code class="language-plaintext highlighter-rouge">DATETIME</code>, <code class="language-plaintext highlighter-rouge">COLLATE CI</code>) is now <strong>refused</strong> instead of quietly breaking <code class="language-plaintext highlighter-rouge">UNIQUE</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5603">#5603</a>). The strategy is also <strong>persisted</strong> now (<a href="https://github.com/ArcadeData/arcadedb/issues/5637">#5637</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CREATE INDEX IF NOT EXISTS</code> answers for the index asked for.</strong> It matched on the property set alone, so a <code class="language-plaintext highlighter-rouge">NOTUNIQUE</code> index answered “already there” to a request for a <code class="language-plaintext highlighter-rouge">UNIQUE</code> one and the constraint was never created (<a href="https://github.com/ArcadeData/arcadedb/issues/5675">#5675</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5765">#5765</a>).</li>
  <li><strong>An existing index is never dropped implicitly any more.</strong> Opt in with <code class="language-plaintext highlighter-rouge">withReplaceIfIncompatible(true)</code>.</li>
  <li><strong>Manual indexes work.</strong> <code class="language-plaintext highlighter-rouge">ManualIndexBuilder.create()</code> registered the wrong object, so the very next commit <strong>fenced the database</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5765">#5765</a>).</li>
  <li><strong>Copying a type copies its records and its index definitions</strong>, not just their names (<a href="https://github.com/ArcadeData/arcadedb/issues/5723">#5723</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">REBUILD INDEX</code> no longer resets a non-default GeoHash <code class="language-plaintext highlighter-rouge">precision</code></strong>, and <strong>an index <code class="language-plaintext highlighter-rouge">METADATA</code> key is now either applied or reported</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5639">#5639</a>); four dense-vector settings that were unreachable behind that silence (<code class="language-plaintext highlighter-rouge">efSearch</code>, <code class="language-plaintext highlighter-rouge">inactivityRebuildTimeoutMs</code>, <code class="language-plaintext highlighter-rouge">neighborOverflowFactor</code>, <code class="language-plaintext highlighter-rouge">alphaDiversityRelaxation</code>) are now settable and persisted.</li>
  <li><strong>Full-text BM25 is comparable across buckets</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5267">#5267</a>), and a combined full-text index expands per property (<a href="https://github.com/ArcadeData/arcadedb/issues/5181">#5181</a>).</li>
  <li><strong>Bounded range scans over multi-series compacted indexes</strong> no longer drop rows or mis-order descending results (<a href="https://github.com/ArcadeData/arcadedb/issues/5214">#5214</a>), an accented partial prefix no longer returns rows of other keys (<a href="https://github.com/ArcadeData/arcadedb/issues/5321">#5321</a>), and a mixed-type index range is bounded by type category (<a href="https://github.com/ArcadeData/arcadedb/issues/5225">#5225</a>).</li>
  <li><strong>A <code class="language-plaintext highlighter-rouge">STRING</code> property no longer reads back as a geometry.</strong> Deserializing any string whose first characters were <code class="language-plaintext highlighter-rouge">POINT</code>, <code class="language-plaintext highlighter-rouge">POLYGON</code> and so on parsed it as WKT and handed back a <code class="language-plaintext highlighter-rouge">Shape</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5600">#5600</a>).</li>
</ul>

<h3 id="vector-search">Vector Search</h3>

<ul>
  <li><strong>A search aimed at a deleted region finds the survivors instead of nothing.</strong> Two independent causes: the traversal was told every node was an acceptable answer, so a beam that filled with tombstones declared itself finished; and a tombstone was scored through a placeholder vector whose cosine similarity came back <code class="language-plaintext highlighter-rouge">Infinity</code>, making every tombstone the <em>best</em> candidate in the beam (<a href="https://github.com/ArcadeData/arcadedb/issues/5558">#5558</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">countEntries()</code> is no longer torn by a concurrent graph rebuild</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5568">#5568</a>).</li>
  <li><strong>The location cache is gone and <code class="language-plaintext highlighter-rouge">locationCacheSize</code> is refused.</strong> It was never a cache bound: an evicted location is unrecoverable and every reader reads a missing location as <strong>deleted</strong>, so a cap of 100 over 1000 live vectors made <code class="language-plaintext highlighter-rouge">countEntries()</code> report 100 and dropped neighbours from searches (<a href="https://github.com/ArcadeData/arcadedb/issues/5559">#5559</a>).</li>
  <li><strong>In-memory bloat is fixed</strong>: <code class="language-plaintext highlighter-rouge">VectorLocation</code> objects were never removed from the map, an OOM after weeks of operation (<a href="https://github.com/ArcadeData/arcadedb/issues/5516">#5516</a>), and <code class="language-plaintext highlighter-rouge">LSMVectorIndex.remove()</code> no longer scans every vector id per call (<a href="https://github.com/ArcadeData/arcadedb/issues/5318">#5318</a>).</li>
  <li><strong>A committed vector is no longer intermittently missing from search</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5615">#5615</a>), and <code class="language-plaintext highlighter-rouge">GraphSearcherPool</code> can no longer hand out a searcher bound to a replaced graph (<a href="https://github.com/ArcadeData/arcadedb/issues/5648">#5648</a>).</li>
  <li><strong>Sparse vectors</strong>: compaction no longer fails past the 2GB WAL buffer (<a href="https://github.com/ArcadeData/arcadedb/issues/5189">#5189</a>), the top-K heap no longer allocates a <code class="language-plaintext highlighter-rouge">Float</code> on every comparison (<a href="https://github.com/ArcadeData/arcadedb/issues/5473">#5473</a>), and block-max skipping cuts the p50 that tracked total posting length (<a href="https://github.com/ArcadeData/arcadedb/issues/5388">#5388</a>).</li>
  <li><strong>Studio can create a vector index</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5607">#5607</a>), and <code class="language-plaintext highlighter-rouge">dimensions</code> is now enforced at creation: an index created without it accepted writes and indexed <strong>nothing</strong>, forever, without a warning.</li>
</ul>

<h3 id="timeseries">TimeSeries</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">DATE</code> / <code class="language-plaintext highlighter-rouge">DATETIME_*</code> / <code class="language-plaintext highlighter-rouge">DECIMAL</code> / <code class="language-plaintext highlighter-rouge">BINARY</code> fields no longer silently corrupt the next column</strong>, and the sealed layer restores the declared column type (<a href="https://github.com/ArcadeData/arcadedb/issues/5475">#5475</a>).</li>
  <li><strong>Unbounded last-point-per-tag no longer scans the whole series</strong> (208 ms to 2.5 ms) via a per-shard latest-ts shortcut (<a href="https://github.com/ArcadeData/arcadedb/issues/5414">#5414</a>), and an unbounded descending scan no longer reads the whole unsealed tail (<a href="https://github.com/ArcadeData/arcadedb/issues/5416">#5416</a>).</li>
  <li><strong>Ingest no longer boxes every numeric sample</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5474">#5474</a>).</li>
</ul>

<h3 id="ha--raft-clustering">HA / Raft Clustering</h3>

<ul>
  <li><strong>A materialized view no longer makes the leader ship page versions followers never received</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5492">#5492</a>), the <code class="language-plaintext highlighter-rouge">WALVersionGapException</code> / non-converging-resync / lost-write shape. Both SQL and Cypher statements now execute against the <strong>replicated</strong> database instance rather than the inner <code class="language-plaintext highlighter-rouge">LocalDatabase</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5655">#5655</a>).</li>
  <li><strong>A follower’s index can no longer be short after an LSM compaction</strong>, which silently returned fewer rows (<a href="https://github.com/ArcadeData/arcadedb/issues/5443">#5443</a>).</li>
  <li><strong>Concurrent transactions on a shared follower <code class="language-plaintext highlighter-rouge">Database</code> handle no longer corrupt records or lose committed writes</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5503">#5503</a>).</li>
  <li><strong>A leader crash between the Raft commit and the phase-2 apply no longer loses a locally-originated write</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5407">#5407</a>), and the phase-2 ticket is released when an abandoned entry applies (<a href="https://github.com/ArcadeData/arcadedb/issues/5410">#5410</a>).</li>
  <li><strong>The Raft log no longer grows unbounded</strong> until disk-full on low-write clusters (<a href="https://github.com/ArcadeData/arcadedb/issues/5345">#5345</a>), and a permanently wedged follower replication channel now escalates instead of staying dead until a leader restart (<a href="https://github.com/ArcadeData/arcadedb/issues/5346">#5346</a>).</li>
  <li><strong>The leader is no longer advertised as available before it is ready to serve</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5453">#5453</a>), and <code class="language-plaintext highlighter-rouge">DROP DATABASE</code> no longer deletes files synchronously inside the Raft apply (<a href="https://github.com/ArcadeData/arcadedb/issues/5454">#5454</a>).</li>
  <li><strong>Kubernetes</strong>: a recreated follower is no longer permanently stranded (<a href="https://github.com/ArcadeData/arcadedb/issues/5268">#5268</a>), a peer is no longer silently removed from the Raft configuration when its pod is deleted (<a href="https://github.com/ArcadeData/arcadedb/issues/5275">#5275</a>), a stranded follower’s Raft server is no longer left <code class="language-plaintext highlighter-rouge">CLOSED</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5271">#5271</a>), the crash-loop on <code class="language-plaintext highlighter-rouge">newTI &lt; oldTI</code> is gone (<a href="https://github.com/ArcadeData/arcadedb/issues/5291">#5291</a>), and the default <code class="language-plaintext highlighter-rouge">raftStorageDirectory</code> lands <strong>inside</strong> <code class="language-plaintext highlighter-rouge">server.databaseDirectory</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5272">#5272</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5277">#5277</a>).</li>
  <li><strong>Cluster status is accurate</strong>: the configuration is re-emitted rather than logged once at bootstrap (<a href="https://github.com/ArcadeData/arcadedb/issues/5304">#5304</a>), the <code class="language-plaintext highlighter-rouge">LATENCY</code> column reports replication RTT rather than heartbeat age (<a href="https://github.com/ArcadeData/arcadedb/issues/5314">#5314</a>), a never-appended follower is no longer reported <code class="language-plaintext highlighter-rouge">HEALTHY</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5295">#5295</a>), and <code class="language-plaintext highlighter-rouge">POST /api/v1/cluster/leader</code> validates its body (<a href="https://github.com/ArcadeData/arcadedb/issues/5276">#5276</a>).</li>
  <li><strong>Polymorphic <code class="language-plaintext highlighter-rouge">count(*) FROM V</code></strong> no longer returns node-dependent totals from bucket counter drift (<a href="https://github.com/ArcadeData/arcadedb/issues/5297">#5297</a>), and parameterized Gremlin commands work on followers (<a href="https://github.com/ArcadeData/arcadedb/issues/5187">#5187</a>).</li>
  <li><strong>A failed startup no longer leaves an unkillable JVM</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5450">#5450</a>), non-daemon background threads no longer keep an embedder JVM alive after a leaked <code class="language-plaintext highlighter-rouge">Database</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5418">#5418</a>), and <code class="language-plaintext highlighter-rouge">schema.load()</code> no longer runs before <code class="language-plaintext highlighter-rouge">checkForRecovery()</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5325">#5325</a>).</li>
</ul>

<h3 id="opencypher">OpenCypher</h3>

<ul>
  <li><strong>A hop onto an already-bound vertex counts every relationship joining the pair.</strong> The optimizer’s operator for such a hop was built as a semi-join, so it answered once per input row and threw the rest of the pair away. The shape where it shows is a <strong>cycle</strong>, whose closing hop always has both endpoints bound: anything aggregating over such a pattern under-reported wherever parallel edges exist between a pair, which is normal in transaction and payment graphs (<a href="https://github.com/ArcadeData/arcadedb/issues/5663">#5663</a>). <strong>Row counts can go up, and that is the fix.</strong></li>
  <li><strong>An unbound <code class="language-plaintext highlighter-rouge">$parameter</code> is an error, not null.</strong> A query referencing a <code class="language-plaintext highlighter-rouge">$name</code> the caller never bound evaluated it to null and ran to completion against a value nobody supplied, so a de-duplicating <code class="language-plaintext highlighter-rouge">WHERE NOT EXISTS { ... } CREATE</code> guard degraded into an unconditional <code class="language-plaintext highlighter-rouge">CREATE</code>. ArcadeDB now raises Neo4j’s own <code class="language-plaintext highlighter-rouge">Expected parameter(s): id</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5501">#5501</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5561">#5561</a>).</li>
  <li><strong>A subquery body is part of the query, not a string it carries.</strong> <code class="language-plaintext highlighter-rouge">EXISTS { }</code>, <code class="language-plaintext highlighter-rouge">COUNT { }</code> and <code class="language-plaintext highlighter-rouge">COLLECT { }</code> held their body as <strong>text</strong>, edited it once per outer row to correlate it, ran it as a standalone statement and <strong>absorbed any failure into the expression’s neutral value</strong> (<code class="language-plaintext highlighter-rouge">false</code>, <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">[]</code>) (<a href="https://github.com/ArcadeData/arcadedb/issues/5656">#5656</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5657">#5657</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5658">#5658</a>). Bodies are ASTs now, run with the outer row as a seed, and a failing body is reported (<a href="https://github.com/ArcadeData/arcadedb/issues/5626">#5626</a>).</li>
  <li><strong>A non-numeric argument to <code class="language-plaintext highlighter-rouge">abs()</code> and friends is a 400, not a 500</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5484">#5484</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5602">#5602</a>), with the message phrased in the vocabulary of the language: <code class="language-plaintext highlighter-rouge">Type mismatch: abs() expects an INTEGER or a FLOAT argument but got STRING</code>.</li>
  <li><strong>Arithmetic errors are client errors.</strong> 64-bit overflow, division and modulo by zero answer HTTP <code class="language-plaintext highlighter-rouge">400</code> and Bolt’s <code class="language-plaintext highlighter-rouge">Neo.ClientError.Statement.ArithmeticError</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5545">#5545</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5647">#5647</a>). Floating-point is untouched.</li>
  <li><strong>The two count push-downs agree.</strong> <code class="language-plaintext highlighter-rouge">RETURN count(*) LIMIT 0</code> returned a row; a pattern that cannot match cost 200 record reads to answer 0; <code class="language-plaintext highlighter-rouge">MATCH (a)-[:LINKS]-&gt;(b) RETURN count(*)</code> with an unlabelled anchor answered <strong>0</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5715">#5715</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5686">#5686</a>).</li>
  <li><strong>Inline <code class="language-plaintext highlighter-rouge">WHERE</code> predicates apply everywhere they are written</strong>: in <code class="language-plaintext highlighter-rouge">MATCH</code>, in <code class="language-plaintext highlighter-rouge">EXISTS { }</code>, in pattern comprehensions, on variable-length patterns and in both <code class="language-plaintext highlighter-rouge">shortestPath</code> evaluators (<a href="https://github.com/ArcadeData/arcadedb/issues/5460">#5460</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5462">#5462</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5463">#5463</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5464">#5464</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5480">#5480</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5481">#5481</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5489">#5489</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5490">#5490</a>).</li>
  <li><strong>Planner</strong>: an inline property map is planned as the equality predicate it stands for (<a href="https://github.com/ArcadeData/arcadedb/issues/5446">#5446</a>); a bound-target expansion filters on the segment’s neighbour pointer (<a href="https://github.com/ArcadeData/arcadedb/issues/5660">#5660</a>); composite indexes are seeked by their whole key (<a href="https://github.com/ArcadeData/arcadedb/issues/5444">#5444</a>); and bounded variable-length paths use indexed and IN-list anchors (<a href="https://github.com/ArcadeData/arcadedb/issues/5357">#5357</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5387">#5387</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5393">#5393</a>).</li>
</ul>

<h3 id="sql">SQL</h3>

<ul>
  <li><strong>Integer arithmetic fails on overflow instead of wrapping</strong>, and division by zero is a client error (<a href="https://github.com/ArcadeData/arcadedb/issues/5164">#5164</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5647">#5647</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5494">#5494</a>).</li>
  <li><strong>Back-tick quoted names containing a backslash are no longer mis-parsed.</strong> A name that arrived already escaped grew one backslash on every parse and re-emission until it no longer resolved.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">MATCHES</code> works when the regular expression contains multiple dots</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5258">#5258</a>), the <code class="language-plaintext highlighter-rouge">MATCH</code> rid filter is no longer discarded (<a href="https://github.com/ArcadeData/arcadedb/issues/5315">#5315</a>), and <code class="language-plaintext highlighter-rouge">TRAVERSE</code>/<code class="language-plaintext highlighter-rouge">SELECT</code> from a bound RID collection no longer NPEs (<a href="https://github.com/ArcadeData/arcadedb/issues/5505">#5505</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">UPDATE ... MERGE</code> supports parameterized payloads</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5347">#5347</a>).</li>
  <li><strong>A large batch script no longer dies with <code class="language-plaintext highlighter-rouge">StackOverflowError</code></strong> when closing its execution plan (<a href="https://github.com/ArcadeData/arcadedb/issues/5708">#5708</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/5720">#5720</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">ceil()</code>/<code class="language-plaintext highlighter-rouge">floor()</code> return FLOAT</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5382">#5382</a>), <code class="language-plaintext highlighter-rouge">||</code> rejects non-STRING operands (<a href="https://github.com/ArcadeData/arcadedb/issues/5298">#5298</a>), chained comparisons are honoured (<a href="https://github.com/ArcadeData/arcadedb/issues/5284">#5284</a>), <code class="language-plaintext highlighter-rouge">split()</code> with an empty delimiter no longer appends a spurious element (<a href="https://github.com/ArcadeData/arcadedb/issues/5390">#5390</a>), and <code class="language-plaintext highlighter-rouge">datetime(map)</code> honours <code class="language-plaintext highlighter-rouge">epochSeconds</code>/<code class="language-plaintext highlighter-rouge">epochMillis</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5274">#5274</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">dateTimeImplementation=java.time.Instant</code> no longer breaks reading DATETIME values.</strong> The moment a DATETIME column crossed the JSON boundary it threw <code class="language-plaintext highlighter-rouge">UnsupportedTemporalTypeException: Unsupported field: YearOfEra</code>, which took out HTTP, the remote driver, Studio, <code class="language-plaintext highlighter-rouge">toJSON()</code> and the SQL <code class="language-plaintext highlighter-rouge">.format()</code> method.</li>
</ul>

<h3 id="server-http-and-observability">Server, HTTP and Observability</h3>

<ul>
  <li><strong>A truncated query response is no longer indistinguishable from a complete one.</strong> The HTTP endpoints serialize at most 20,000 rows and reported that nowhere: same <code class="language-plaintext highlighter-rouge">200</code>, same body shape (<a href="https://github.com/ArcadeData/arcadedb/issues/5711">#5711</a>). A <code class="language-plaintext highlighter-rouge">limit</code> the caller states is now honored as written, a query’s own <code class="language-plaintext highlighter-rouge">LIMIT</code> raises the cap, and the response carries <code class="language-plaintext highlighter-rouge">{"limit": 20000, "returned": 20000, "truncated": true}</code>. <code class="language-plaintext highlighter-rouge">RemoteDatabase.setMaxResultRows(Integer)</code> sets it per connection; Studio marks the row count <code class="language-plaintext highlighter-rouge">(truncated)</code>.</li>
  <li><strong>Query and HTTP metrics survive an in-process server restart</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5565">#5565</a>).</li>
  <li><strong>Unexpected internal server errors stay visible in production-mode logs</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5374">#5374</a>), with the full stack trace.</li>
  <li><strong>HTTP status classification</strong>: client errors are classified on the Postgres, Redis, MongoDB and GraphQL wire paths (<a href="https://github.com/ArcadeData/arcadedb/issues/5628">#5628</a>), the command API no longer answers <code class="language-plaintext highlighter-rouge">500 ClassCastException</code> for a non-string <code class="language-plaintext highlighter-rouge">language</code> or <code class="language-plaintext highlighter-rouge">command</code> field (<a href="https://github.com/ArcadeData/arcadedb/issues/5222">#5222</a>), and JSON array payloads are accepted (<a href="https://github.com/ArcadeData/arcadedb/issues/5415">#5415</a>).</li>
  <li><strong>The “not found” message for a missing bucket reaches the user again</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5636">#5636</a>). <code class="language-plaintext highlighter-rouge">Schema</code> now exposes null-returning <code class="language-plaintext highlighter-rouge">getBucketByIdIfExists(int)</code> / <code class="language-plaintext highlighter-rouge">getBucketByNameIfExists(String)</code>.</li>
  <li><strong>The remote driver honors the server’s 503 “please retry”</strong>, and the console no longer terminates a comment on a semicolon (<a href="https://github.com/ArcadeData/arcadedb/issues/5457">#5457</a>).</li>
</ul>

<h3 id="wire-protocols">Wire Protocols</h3>

<ul>
  <li><strong>PostgreSQL</strong>: quoted identifiers are identifiers, not string literals (<a href="https://github.com/ArcadeData/arcadedb/issues/5369">#5369</a>); the schema path no longer collapses <code class="language-plaintext highlighter-rouge">ARRAY_OF_*</code> / <code class="language-plaintext highlighter-rouge">DATETIME_*</code> to <code class="language-plaintext highlighter-rouge">VARCHAR</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5311">#5311</a>); <code class="language-plaintext highlighter-rouge">LIST OF EMBEDDED</code> is advertised as <code class="language-plaintext highlighter-rouge">json[]</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/5289">#5289</a>); nested arrays serialize (<a href="https://github.com/ArcadeData/arcadedb/issues/5366">#5366</a>); and a single-column projection no longer returns every column (<a href="https://github.com/ArcadeData/arcadedb/issues/5367">#5367</a>).</li>
  <li><strong>Gremlin</strong>: a traversal result carrying a raw RID serializes (<a href="https://github.com/ArcadeData/arcadedb/issues/5309">#5309</a>); <code class="language-plaintext highlighter-rouge">ArcadeGraphManager</code> no longer caches a stale graph after the underlying database is reopened (<a href="https://github.com/ArcadeData/arcadedb/issues/5307">#5307</a>); <code class="language-plaintext highlighter-rouge">hasLabel()</code> no longer returns elements of the wrong kind (<a href="https://github.com/ArcadeData/arcadedb/issues/5223">#5223</a>).</li>
  <li><strong>Bolt</strong>: two concurrency defects behind the flaky <code class="language-plaintext highlighter-rouge">concurrentSessions</code> are fixed, and a missing parameter answers <code class="language-plaintext highlighter-rouge">Neo.ClientError.Statement.ParameterMissing</code> rather than <code class="language-plaintext highlighter-rouge">SyntaxError</code>.</li>
</ul>

<h2 id="breaking-changes-and-upgrade-notes">Breaking Changes and Upgrade Notes</h2>

<p><strong>No schema migration is required and no existing database is rewritten.</strong> The items below change behaviour that existing code or scripts may depend on. The full list is in the <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.8.1">release notes</a>.</p>

<h3 id="sql-and-cypher">SQL and Cypher</h3>

<ul>
  <li><strong>Inside a back-tick quoted name, a backslash escapes the next character.</strong> A literal backslash has to be doubled. Only names that actually contain a backslash are affected.</li>
  <li><strong>Cypher: an unbound <code class="language-plaintext highlighter-rouge">$parameter</code> raises</strong> <code class="language-plaintext highlighter-rouge">Expected parameter(s): x</code> instead of evaluating to null. To keep the old behaviour for a specific query, bind the name explicitly to <code class="language-plaintext highlighter-rouge">null</code>.</li>
  <li><strong>Cypher: a failing <code class="language-plaintext highlighter-rouge">EXISTS { }</code> / <code class="language-plaintext highlighter-rouge">COUNT { }</code> / <code class="language-plaintext highlighter-rouge">COLLECT { }</code> body returns its error</strong> instead of the expression’s neutral value. The old answer was wrong, not merely quiet.</li>
  <li><strong>Cypher: parse-time validation reaches every clause and every subquery body.</strong> A query whose bad call sits in a clause the validation never walked, or in a branch that never executes, is now rejected before it starts.</li>
  <li><strong>Cypher: a wrong argument count now raises <code class="language-plaintext highlighter-rouge">CommandSemanticException</code></strong> (a <code class="language-plaintext highlighter-rouge">CommandParsingException</code> subclass). Embedded code catching <code class="language-plaintext highlighter-rouge">CommandExecutionException</code> around a call should catch <code class="language-plaintext highlighter-rouge">CommandParsingException</code>.</li>
  <li><strong>Cypher: row counts can go up</strong> where parallel edges join a pair and the pattern has a hop onto a bound vertex (typically a cycle). A saved report or a threshold calibrated against the old numbers should be re-checked.</li>
</ul>

<h3 id="indexes-1">Indexes</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">CREATE INDEX ... METADATA</code> refuses an unknown or malformed key</strong>, on <code class="language-plaintext highlighter-rouge">LSM_VECTOR</code>, <code class="language-plaintext highlighter-rouge">LSM_SPARSE_VECTOR</code> and <code class="language-plaintext highlighter-rouge">FULL_TEXT</code>. A stored migration carrying a stray or misspelled key used to run and is now refused, which is the point, since the key was never doing anything.</li>
  <li><strong>A guarded <code class="language-plaintext highlighter-rouge">CREATE INDEX IF NOT EXISTS</code> naming a setting may now raise where it used to be a no-op</strong>, if the index already there carries a different value for any key the clause names. Drop the keys you do not actually require, or align the value.</li>
  <li><strong>An existing <code class="language-plaintext highlighter-rouge">EUCLIDEAN</code> or <code class="language-plaintext highlighter-rouge">DOT_PRODUCT</code> vector index changes its search results on the first reopen.</strong> The persisted definition names the metric <code class="language-plaintext highlighter-rouge">similarityFunction</code> while the reader looked only for <code class="language-plaintext highlighter-rouge">similarity</code>, so such an index has been <em>scoring</em> with COSINE since the first restart after it was created. It now scores with the metric it was created with. Nothing to re-create or rebuild. COSINE indexes are unaffected.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">arcadedb.vectorIndex.locationCacheSize</code> and the per-index <code class="language-plaintext highlighter-rouge">locationCacheSize</code> metadata are refused</strong> for any positive value. <strong>Remove the key from any <code class="language-plaintext highlighter-rouge">CREATE INDEX</code> script before upgrading.</strong> Plan for <strong>~90 bytes per live vector</strong>, the figure <code class="language-plaintext highlighter-rouge">getStats()</code> now reports as <code class="language-plaintext highlighter-rouge">estimatedLocationIndexBytes</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">getOrCreateTypeIndex</code> no longer upgrades an incompatible index</strong>: it raises <code class="language-plaintext highlighter-rouge">IllegalArgumentException</code> naming both definitions instead of dropping and rebuilding. Use <code class="language-plaintext highlighter-rouge">buildTypeIndex(...).withReplaceIfIncompatible(true)</code> if replacing really is what you mean.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">withPageSize(0)</code> now means “use the default”</strong>, and <strong><code class="language-plaintext highlighter-rouge">withPageSize(262_144)</code> on a <code class="language-plaintext highlighter-rouge">HASH</code> index is now refused</strong> with a message naming the 256-65536 range.</li>
  <li><strong>A <code class="language-plaintext highlighter-rouge">LINK</code> HASH index created with this release is not readable by an earlier build.</strong> It has to be dropped before downgrading.</li>
</ul>

<h3 id="metrics-and-reported-statistics">Metrics and Reported Statistics</h3>

<ul>
  <li><strong>The monotonic engine metrics are Prometheus counters now</strong>, so the exported series are renamed with a <code class="language-plaintext highlighter-rouge">_total</code> suffix: <code class="language-plaintext highlighter-rouge">arcadedb_engine_page_cache_hits_total</code>, <code class="language-plaintext highlighter-rouge">arcadedb_engine_pages_read_total</code>, <code class="language-plaintext highlighter-rouge">arcadedb_engine_wal_bytes_written_total</code>, <code class="language-plaintext highlighter-rouge">arcadedb_engine_mvcc_conflicts_total</code>, <code class="language-plaintext highlighter-rouge">arcadedb_engine_queries_total</code> and the rest. <strong>Existing dashboards and alerts on the old names need updating.</strong> The three genuinely instantaneous readings (<code class="language-plaintext highlighter-rouge">wal_files</code>, <code class="language-plaintext highlighter-rouge">files_open</code>, <code class="language-plaintext highlighter-rouge">databases</code>) stay gauges and keep their names.</li>
  <li><strong>A server restart in the same JVM resets the query and HTTP counters</strong>, because the values belong to the server that recorded them.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CHECK DATABASE</code> output changed</strong>: a corrupt vertex or edge produces <strong>one</strong> warning per record instead of two, and <code class="language-plaintext highlighter-rouge">totalWarnings</code> now counts distinct messages rather than occurrences.</li>
</ul>

<h3 id="java-api">Java API</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">Schema</code> gains <code class="language-plaintext highlighter-rouge">getBucketByIdIfExists(int)</code> and <code class="language-plaintext highlighter-rouge">getBucketByNameIfExists(String)</code></strong>, source-incompatible for anyone implementing <code class="language-plaintext highlighter-rouge">com.arcadedb.schema.Schema</code> outside the project.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">AfterRecordReadListener</code> must return a mutable record</strong> when returning a different one than it was handed (typically <code class="language-plaintext highlighter-rouge">record.modify()</code>) (<a href="https://github.com/ArcadeData/arcadedb/issues/5755">#5755</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">BucketLSMVectorIndexBuilder</code> no longer exposes its settings as public fields.</strong> Every fluent <code class="language-plaintext highlighter-rouge">withX()</code> method is preserved, with <code class="language-plaintext highlighter-rouge">withEfSearch</code> added.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">TypeLSMVectorIndexBuilder.withLocationCacheSize(N)</code> is deprecated</strong> and refuses a positive <code class="language-plaintext highlighter-rouge">N</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">BucketSelectionStrategy.getBucketIdByKeys(List, Object[], boolean)</code></strong> is the new contract; the single-argument form and <code class="language-plaintext highlighter-rouge">DocumentType.getBucketIndexByKeys(Object[], boolean)</code> are deprecated and never prune.</li>
</ul>

<h3 id="operational">Operational</h3>

<ul>
  <li><strong>Partitioned types</strong>: a database that ran <code class="language-plaintext highlighter-rouge">partitioned(...)</code> on a type carrying more than one index may already hold duplicates in a secondary <code class="language-plaintext highlighter-rouge">UNIQUE</code> index. The constraint is enforced again from this release, but existing rows are not retro-validated: check those indexes and <code class="language-plaintext highlighter-rouge">REBUILD INDEX</code> them.</li>
  <li><strong>Geospatial</strong>: existing indexes keep the old layout and are not rewritten, but they change <em>query</em> behaviour the moment the jar is swapped. Run <code class="language-plaintext highlighter-rouge">REBUILD INDEX `Type[prop]`</code> to get the ingest and selectivity gains.</li>
  <li><strong>Rolling HA upgrade</strong>: upgrade followers before, or together with, the leader, because of the multi-page schema dictionary.</li>
</ul>

<h2 id="dependency-updates">Dependency Updates</h2>

<p>Around 80 dependency bumps landed in this cycle, almost all through Dependabot. The notable ones: the Jackson family pinned to <strong>2.22.1</strong> via <code class="language-plaintext highlighter-rouge">jackson-bom</code>, <strong>Ivy</strong> raised to 2.6.0 to clear CVE-2026-26032, GraalVM <strong>25.2.4</strong>, JVector <strong>4.0.0-rc.9</strong>, Groovy <strong>4.0.33</strong> and Logback <strong>1.6.1</strong>. Two versions are deliberately <strong>frozen</strong> and documented as such: the Gremlin ANTLR runtime and Groovy majors, which TinkerPop cannot take.</p>

<h2 id="getting-started-with-2681">Getting Started with 26.8.1</h2>

<h3 id="docker">Docker</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull arcadedata/arcadedb:26.8.1
</code></pre></div></div>

<p>Visit our <a href="https://hub.docker.com/r/arcadedata/arcadedb">Docker Hub repository</a> for more information.</p>

<h3 id="maven">Maven</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>com.arcadedb<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>arcadedb-engine<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;version&gt;</span>26.8.1<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>All artifacts are available on <a href="https://repo.maven.apache.org/maven2/com/arcadedb/">Maven Central</a>.</p>

<h3 id="documentation">Documentation</h3>

<p>For details on features and usage, see the <a href="https://docs.arcadedb.com/">documentation</a>.</p>

<h2 id="compatibility-note">Compatibility Note</h2>

<p>This release requires no schema migration and rewrites no existing database, so no export or import is needed when upgrading. It does contain behaviour changes, collected in the section above. As always, we recommend creating a database backup before upgrading.</p>

<hr />

<p><strong>Download ArcadeDB 26.8.1 now</strong>: <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.8.1">GitHub Releases</a></p>

<p>Thanks to everyone who reported, reproduced, reviewed, tested and fixed, in particular <a href="https://github.com/adepase">@adepase</a>, <a href="https://github.com/alphafarmer">@alphafarmer</a>, <a href="https://github.com/borutjures">@borutjures</a>, <a href="https://github.com/cmettier">@cmettier</a>, <a href="https://github.com/danieljuhl">@danieljuhl</a>, <a href="https://github.com/focusmacula">@focusmacula</a>, <a href="https://github.com/gramian">@gramian</a>, <a href="https://github.com/ironluca">@ironluca</a>, <a href="https://github.com/ivanfrias">@ivanfrias</a>, <a href="https://github.com/justinblethrow-cloud">@justinblethrow-cloud</a>, <a href="https://github.com/kl-demi">@kl-demi</a>, <a href="https://github.com/KyaniteSolutions">@KyaniteSolutions</a>, <a href="https://github.com/LepsyMikolaj3301">@LepsyMikolaj3301</a>, <a href="https://github.com/mdre">@mdre</a>, <a href="https://github.com/rthuffman">@rthuffman</a>, <a href="https://github.com/ruispereira">@ruispereira</a>, <a href="https://github.com/Rupert1987">@Rupert1987</a>, <a href="https://github.com/shulei5831sl">@shulei5831sl</a>, <a href="https://github.com/sunil-pateel">@sunil-pateel</a>, <a href="https://github.com/tae898">@tae898</a>, <a href="https://github.com/TobiasJoseHermann">@TobiasJoseHermann</a>, <a href="https://github.com/vivekjustthink">@vivekjustthink</a>, <a href="https://github.com/waterWang">@waterWang</a>, <a href="https://github.com/xdevsapps">@xdevsapps</a> and <a href="https://github.com/YaeSakuraQ">@YaeSakuraQ</a>.</p>

<p>Luca Garulli
ArcadeDB Founder</p>]]></content><author><name>Luca Garulli</name></author><category term="Multi-Model" /><category term="Concurrency" /><category term="Security" /><category term="Graph Database" /><category term="Vector Search" /><category term="MCP" /><category term="Release" /><summary type="html"><![CDATA[ArcadeDB 26.8.1 is a major release: 381 issues and pull requests closed, 669 commits. Concurrent writes to unrelated records of the same page no longer conflict, vertex deletes no longer lose edges and are 5x faster, bloom filters land on compacted LSM indexes, and 13 security advisories are closed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/release-v26.8.1.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/release-v26.8.1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building Memory for LLM Agents with ArcadeDB</title><link href="https://arcadedb.com/blog/building-memory-for-llm-agents-with-arcadedb/" rel="alternate" type="text/html" title="Building Memory for LLM Agents with ArcadeDB" /><published>2026-07-30T00:00:00+00:00</published><updated>2026-07-30T00:00:00+00:00</updated><id>https://arcadedb.com/blog/building-memory-for-llm-agents-with-arcadedb</id><content type="html" xml:base="https://arcadedb.com/blog/building-memory-for-llm-agents-with-arcadedb/"><![CDATA[<p><strong>An agent that cannot remember is a chatbot with extra steps.</strong> This post shows how to build persistent memory for an LLM agent on a knowledge graph: storing episodes and the entities they mention, keeping facts current without losing their history, and recalling context with vector similarity and graph traversal in one query.</p>

<p>Everything here runs in-process with <code class="language-plaintext highlighter-rouge">pip install arcadedb-embedded</code>. No server, no separate vector database.</p>

<hr />

<h2 id="what-agent-memory-actually-is">What Agent Memory Actually Is</h2>

<p>“Memory” gets used loosely. In practice an agent needs three different things, and only two of them belong in a database.</p>

<p><strong>Working memory</strong> is the context window: the current conversation, tool outputs, and scratchpad. It lives in the prompt and disappears when the request ends.</p>

<p><strong>Episodic memory</strong> is what happened. Turn by turn, session by session: the user asked X, the agent called tool Y, the result was Z. It is append-only and inherently temporal.</p>

<p><strong>Semantic memory</strong> is what the agent has learned. Durable facts about entities: this user prefers Postgres, this project depends on that service, this customer is on the enterprise plan. Facts change, and the changes matter.</p>

<p>The common mistake is to implement all three as a single vector index over conversation transcripts. That works for the first demo and degrades quickly.</p>

<h2 id="why-a-vector-store-alone-is-not-enough">Why a Vector Store Alone Is Not Enough</h2>

<p>Embedding every turn and retrieving the top-k nearest is a reasonable baseline. It fails in four specific ways, and all four are relationship problems.</p>

<p><strong>Superseded facts.</strong> The user said they work at Acme in March and at Globex in June. Both turns are in the index, both are semantically similar to “where does the user work”, and the retriever has no notion that one replaced the other. The agent confidently returns the wrong employer. A vector index has no concept of validity.</p>

<p><strong>Entity identity.</strong> “Ana”, “Ana Ruiz”, and “a.ruiz@example.com” are the same person. Cosine similarity does not know that. Without entity resolution, memory fragments across aliases and recall silently degrades as the number of sessions grows.</p>

<p><strong>Structural questions.</strong> “What has this user asked about the billing service?” is a filter over a relationship, not a similarity search. A vector index can only approximate it, and approximation is exactly the wrong tool when the answer is deterministic.</p>

<p><strong>Multi-hop recall.</strong> “Which of the user’s projects depends on the library that just broke?” requires traversing project to dependency to incident. No single stored turn contains that chain.</p>

<p>A knowledge graph answers all four directly, because it stores the relationships instead of hoping similarity implies them. What it is not good at on its own is fuzzy recall, which is why the useful design keeps both, in one engine.</p>

<h2 id="the-schema">The Schema</h2>

<p>Four vertex types and three edge types are enough to start.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">arcadedb_embedded</span> <span class="k">as</span> <span class="n">arcadedb</span>

<span class="n">DIM</span> <span class="o">=</span> <span class="mi">1536</span>  <span class="c1"># match your embedding model
</span>
<span class="n">db_ctx</span> <span class="o">=</span> <span class="n">arcadedb</span><span class="p">.</span><span class="nf">create_database</span><span class="p">(</span><span class="sh">"</span><span class="s">./agent-memory</span><span class="sh">"</span><span class="p">)</span>

<span class="k">with</span> <span class="n">db_ctx</span> <span class="k">as</span> <span class="n">db</span><span class="p">:</span>
    <span class="k">with</span> <span class="n">db</span><span class="p">.</span><span class="nf">transaction</span><span class="p">():</span>
        <span class="c1"># An episode is one thing that happened
</span>        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE VERTEX TYPE Episode</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Episode.content STRING</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Episode.session STRING</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Episode.ts DATETIME</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Episode.embedding LIST OF FLOAT</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># Entities the agent has learned about
</span>        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE VERTEX TYPE Entity</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Entity.name STRING</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Entity.kind STRING</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE INDEX ON Entity (name) UNIQUE</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># A durable fact, valid over an interval
</span>        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE VERTEX TYPE Fact</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Fact.predicate STRING</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Fact.value STRING</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Fact.valid_from DATETIME</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE PROPERTY Fact.valid_to DATETIME</span><span class="sh">"</span><span class="p">)</span>

        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE EDGE TYPE MENTIONS</span><span class="sh">"</span><span class="p">)</span>   <span class="c1"># Episode -&gt; Entity
</span>        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE EDGE TYPE ASSERTS</span><span class="sh">"</span><span class="p">)</span>    <span class="c1"># Episode -&gt; Fact
</span>        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">CREATE EDGE TYPE ABOUT</span><span class="sh">"</span><span class="p">)</span>      <span class="c1"># Fact    -&gt; Entity
</span>
        <span class="c1"># HNSW index for semantic recall over episodes
</span>        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
            <span class="sa">f</span><span class="sh">"</span><span class="s">CREATE INDEX ON Episode (embedding) LSM_VECTOR </span><span class="sh">"</span>
            <span class="sa">f</span><span class="sh">"</span><span class="s">METADATA {{ dimensions: </span><span class="si">{</span><span class="n">DIM</span><span class="si">}</span><span class="s">, similarity: </span><span class="sh">'</span><span class="s">COSINE</span><span class="sh">'</span><span class="s"> }}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>The separation matters: an <code class="language-plaintext highlighter-rouge">Episode</code> is immutable and says what was observed, a <code class="language-plaintext highlighter-rouge">Fact</code> is the interpretation, and the <code class="language-plaintext highlighter-rouge">ASSERTS</code> edge records which episode produced it. When the agent later gets something wrong, you can trace the belief back to the turn that caused it.</p>

<h2 id="writing-a-turn-to-memory">Writing a Turn to Memory</h2>

<p>Each turn does three things: store the episode with its embedding, resolve the entities it mentions, and assert or update any facts.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timezone</span>

<span class="k">def</span> <span class="nf">remember</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">text</span><span class="p">,</span> <span class="n">entities</span><span class="p">,</span> <span class="n">facts</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Persist one turn: episode, entities, and any facts it asserts.</span><span class="sh">"""</span>
    <span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">).</span><span class="nf">isoformat</span><span class="p">()</span>

    <span class="k">with</span> <span class="n">db</span><span class="p">.</span><span class="nf">transaction</span><span class="p">():</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">CREATE VERTEX Episode SET content = ?, session = ?, </span><span class="sh">"</span>
            <span class="sh">"</span><span class="s">ts = ?, embedding = ?</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">text</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">now</span><span class="p">,</span> <span class="nf">embed</span><span class="p">(</span><span class="n">text</span><span class="p">))</span>

        <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">kind</span> <span class="ow">in</span> <span class="n">entities</span><span class="p">:</span>
            <span class="c1"># UPSERT gives one vertex per distinct entity name
</span>            <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">UPDATE Entity SET name = ?, kind = ? UPSERT WHERE name = ?</span><span class="sh">"</span><span class="p">,</span>
                <span class="n">name</span><span class="p">,</span> <span class="n">kind</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span>
            <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">CREATE EDGE MENTIONS </span><span class="sh">"</span>
                <span class="sh">"</span><span class="s">FROM (SELECT FROM Episode WHERE content = ? AND session = ?) </span><span class="sh">"</span>
                <span class="sh">"</span><span class="s">TO (SELECT FROM Entity WHERE name = ?)</span><span class="sh">"</span><span class="p">,</span>
                <span class="n">text</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">subject</span><span class="p">,</span> <span class="n">predicate</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">facts</span><span class="p">:</span>
            <span class="nf">assert_fact</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">subject</span><span class="p">,</span> <span class="n">predicate</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">now</span><span class="p">,</span> <span class="n">source</span><span class="o">=</span><span class="n">text</span><span class="p">,</span>
                        <span class="n">session</span><span class="o">=</span><span class="n">session</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="keeping-facts-current-without-losing-history">Keeping Facts Current Without Losing History</h2>

<p>This is the part a vector store cannot do at all. When a new value arrives for a predicate that already has one, do not overwrite it: close the old fact and open a new one.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">assert_fact</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">subject</span><span class="p">,</span> <span class="n">predicate</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">now</span><span class="p">,</span> <span class="n">source</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Close any currently-valid fact for this predicate, then open a new one.</span><span class="sh">"""</span>
    <span class="c1"># 1. Invalidate the previous value, if any
</span>    <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">UPDATE Fact SET valid_to = ? </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">WHERE predicate = ? AND valid_to IS NULL </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">AND @rid IN (SELECT in(</span><span class="sh">'</span><span class="s">ABOUT</span><span class="sh">'</span><span class="s">).@rid FROM Entity WHERE name = ?)</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">now</span><span class="p">,</span> <span class="n">predicate</span><span class="p">,</span> <span class="n">subject</span><span class="p">)</span>

    <span class="c1"># 2. Record the new value
</span>    <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">CREATE VERTEX Fact SET predicate = ?, value = ?, valid_from = ?</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">predicate</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">now</span><span class="p">)</span>
    <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">CREATE EDGE ABOUT </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">FROM (SELECT FROM Fact WHERE predicate = ? AND value = ? </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">      AND valid_to IS NULL) </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">TO (SELECT FROM Entity WHERE name = ?)</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">predicate</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">subject</span><span class="p">)</span>
    <span class="n">db</span><span class="p">.</span><span class="nf">command</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">CREATE EDGE ASSERTS </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">FROM (SELECT FROM Episode WHERE content = ? AND session = ?) </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">TO (SELECT FROM Fact WHERE predicate = ? AND value = ? </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">    AND valid_to IS NULL)</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">source</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">predicate</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
</code></pre></div></div>

<!-- UNVERIFIED: the correlated subquery in step 1 ("@rid IN (SELECT in('ABOUT').@rid ...)") uses documented SQL primitives but this exact composition has not been executed. Verify before publishing. -->

<p>Now “where does the user work?” has one right answer, and “where did the user work in April?” is still answerable. Nothing was destroyed.</p>

<h2 id="recall-similarity-then-structure">Recall: Similarity, Then Structure</h2>

<p>Retrieval runs in two layers. Semantic recall finds episodes that resemble the question; structural recall pulls the facts that are currently true.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">recall</span><span class="p">(</span><span class="n">db</span><span class="p">,</span> <span class="n">question</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
    <span class="n">q</span> <span class="o">=</span> <span class="nf">embed</span><span class="p">(</span><span class="n">question</span><span class="p">)</span>

    <span class="c1"># 1. Episodes that resemble the question, plus what they mention
</span>    <span class="n">episodes</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">SELECT content, ts, distance, out(</span><span class="sh">'</span><span class="s">MENTIONS</span><span class="sh">'</span><span class="s">).name AS entities </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">FROM ( SELECT expand(vector.neighbors(</span><span class="sh">'</span><span class="s">Episode[embedding]</span><span class="sh">'</span><span class="s">, ?, ?)) )</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>

    <span class="c1"># 2. Facts that are currently true about those entities
</span>    <span class="n">facts</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="sh">"</span><span class="s">sql</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">SELECT predicate, value, out(</span><span class="sh">'</span><span class="s">ABOUT</span><span class="sh">'</span><span class="s">).name AS subject </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">FROM Fact WHERE valid_to IS NULL </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">AND out(</span><span class="sh">'</span><span class="s">ABOUT</span><span class="sh">'</span><span class="s">).name IN ?</span><span class="sh">"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">e</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">episodes</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="p">(</span><span class="n">row</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">entities</span><span class="sh">"</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[])])</span>

    <span class="k">return</span> <span class="n">episodes</span><span class="p">,</span> <span class="n">facts</span>
</code></pre></div></div>

<!-- UNVERIFIED: composing vector.neighbors() with an out('MENTIONS') traversal in the outer SELECT uses documented primitives but this exact composition is not shown in the docs and has not been executed. Verify before publishing. -->

<p>The second query is the one that makes the agent reliable. It returns only facts whose <code class="language-plaintext highlighter-rouge">valid_to</code> is null, so superseded beliefs cannot leak back into the prompt no matter how semantically similar the old episode was.</p>

<h2 id="wiring-it-into-a-framework">Wiring It Into a Framework</h2>

<p>If you already drive your agent through a framework, you do not need to hand-roll the retrieval layer. The shape of that layer, graph traversal plus vector search in one store, is what the <a href="/graph-rag.html">Graph RAG page</a> covers.</p>

<p><strong>LangChain.</strong> The official <a href="https://github.com/ArcadeData/langchain-arcadedb"><code class="language-plaintext highlighter-rouge">langchain-arcadedb</code></a> package provides <code class="language-plaintext highlighter-rouge">ArcadeDBGraph</code>, which implements LangChain’s <code class="language-plaintext highlighter-rouge">GraphStore</code> protocol and connects over ArcadeDB’s Bolt protocol.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">langchain_arcadedb</span> <span class="kn">import</span> <span class="n">ArcadeDBGraph</span>

<span class="n">graph</span> <span class="o">=</span> <span class="nc">ArcadeDBGraph</span><span class="p">(</span>
    <span class="n">url</span><span class="o">=</span><span class="sh">"</span><span class="s">bolt://localhost:7687</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">username</span><span class="o">=</span><span class="sh">"</span><span class="s">root</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">password</span><span class="o">=</span><span class="sh">"</span><span class="s">playwithdata</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">database</span><span class="o">=</span><span class="sh">"</span><span class="s">agent-memory</span><span class="sh">"</span><span class="p">,</span>
<span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">graph</span><span class="p">.</span><span class="n">get_schema</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>LlamaIndex.</strong> <a href="https://pypi.org/project/llama-index-graph-stores-arcadedb/"><code class="language-plaintext highlighter-rouge">llama-index-graph-stores-arcadedb</code></a> implements the LlamaIndex <code class="language-plaintext highlighter-rouge">PropertyGraphStore</code> interface.</p>

<p><strong>MCP.</strong> ArcadeDB ships an MCP server, so an LLM can query the memory graph directly as a tool. See <a href="/blog/arcadedb-mcp-server-connect-your-llm-to-your-database/">connecting your LLM to your database with MCP</a>.</p>

<p>Note that the LangChain path needs a running server with the Bolt plugin enabled, whereas the embedded path above needs no server at all. Both talk to the same storage format.</p>

<h2 id="why-this-runs-well">Why This Runs Well</h2>

<p>Recall sits in the agent’s request path, so traversal speed is not an academic concern. On the LDBC Graphalytics suite, run on identical hardware against seven other graph engines, ArcadeDB completes weakly connected components in 0.08s and PageRank in 0.10s, winning 5 of the 6 standard algorithms. On the LSQB pattern-matching benchmark, Q6, a two-hop traversal counting 1.67 billion rows, finishes in 110ms. Full methodology and the reproducible harness are on the <a href="https://arcadedb.com/benchmarks.html">benchmarks page</a>.</p>

<p>Embedded mode removes the network entirely, which for a single-process agent is usually the largest remaining latency term.</p>

<h2 id="honest-comparison">Honest Comparison</h2>

<p>ArcadeDB is not the only option that stores graph and vectors together. <a href="https://memgraph.com">Memgraph</a> and <a href="https://falkordb.com">FalkorDB</a> both index embeddings in the same store as the graph and both target agent memory explicitly, and either is a reasonable choice.</p>

<p>The differences worth weighing are licensing and breadth. Memgraph Community is BSL 1.1 and FalkorDB is SSPLv1, both source-available rather than OSI open source; ArcadeDB is Apache 2.0. ArcadeDB also stores documents and time series alongside the graph, and runs embedded in your Python or JVM process rather than only as a server. The full side-by-side is in <a href="/blog/open-source-knowledge-graph-graphrag-databases-compared/">open source knowledge graph and GraphRAG databases compared</a>.</p>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<h3 id="what-is-memory-for-an-llm-agent">What is memory for an LLM agent?</h3>

<p>Agent memory is the state an agent keeps between turns and between sessions. It usually splits into episodic memory (what happened, turn by turn), semantic memory (durable facts learned about entities), and working memory (the current context window). Only the first two need a database.</p>

<h3 id="why-is-a-vector-store-alone-not-enough-for-agent-memory">Why is a vector store alone not enough for agent memory?</h3>

<p>A vector store retrieves passages that resemble the query, but it cannot express that a fact was superseded, that two names refer to one person, or that a preference belongs to a project rather than a user. Those are relationships and validity intervals, which is what a graph stores.</p>

<h3 id="how-do-you-stop-an-agent-from-recalling-outdated-facts">How do you stop an agent from recalling outdated facts?</h3>

<p>Do not overwrite facts; invalidate them. Give each fact edge a valid_from and valid_to timestamp, and when a new value arrives, close the old edge instead of deleting it. Retrieval then filters to edges where valid_to is null, and the history stays auditable.</p>

<h3 id="can-arcadedb-store-agent-memory-and-the-embeddings-together">Can ArcadeDB store agent memory and the embeddings together?</h3>

<p>Yes. Embeddings live in a property on the same records that carry the graph, indexed with HNSW. A single transaction writes an episode, its vector, and its relationships, so semantic recall and relationship traversal run against one consistent copy of the data.</p>

<h3 id="does-arcadedb-work-with-langchain-for-agent-memory">Does ArcadeDB work with LangChain for agent memory?</h3>

<p>Yes. The official langchain-arcadedb package provides ArcadeDBGraph, which implements the LangChain GraphStore protocol and connects over ArcadeDB’s Bolt protocol. There is also a LlamaIndex PropertyGraphStore integration, and an MCP server that lets an LLM query the database directly.</p>

<h3 id="can-agent-memory-run-without-a-database-server">Can agent memory run without a database server?</h3>

<p>Yes. The arcadedb-embedded Python package runs the full engine inside your agent process, with a bundled Java runtime. For a single-process agent or a local assistant, that removes the server, the network hop, and the deployment entirely while keeping graph and vector recall.</p>

<hr />

<h2 id="getting-started">Getting Started</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>arcadedb-embedded
</code></pre></div></div>

<p>That is the whole install: the wheel bundles a Java runtime, so there is no JDK to set up. From there, the schema above is a working starting point you can adapt.</p>

<p>For the retrieval patterns behind this, see <a href="https://arcadedb.com/graph-rag.html">GraphRAG on ArcadeDB</a>. For entity extraction and knowledge graph construction, see <a href="https://arcadedb.com/knowledge-graphs.html">knowledge graphs</a>. ArcadeDB is Apache 2.0 and free in production, with no node limits and no enterprise edition.</p>]]></content><author><name>Luca Garulli</name></author><category term="LLM Agents" /><category term="Agent Memory" /><category term="Knowledge Graph" /><category term="GraphRAG" /><category term="Vector Search" /><category term="AI" /><category term="Python" /><category term="LangChain" /><summary type="html"><![CDATA[How to build persistent memory for LLM agents using a knowledge graph: episodic and semantic memory, temporal fact invalidation, and hybrid vector plus graph recall in ArcadeDB.]]></summary></entry><entry><title type="html">Open Source Knowledge Graph &amp;amp; GraphRAG Databases Compared (2026)</title><link href="https://arcadedb.com/blog/open-source-knowledge-graph-graphrag-databases-compared/" rel="alternate" type="text/html" title="Open Source Knowledge Graph &amp;amp; GraphRAG Databases Compared (2026)" /><published>2026-07-30T00:00:00+00:00</published><updated>2026-07-30T00:00:00+00:00</updated><id>https://arcadedb.com/blog/open-source-knowledge-graph-graphrag-databases-compared</id><content type="html" xml:base="https://arcadedb.com/blog/open-source-knowledge-graph-graphrag-databases-compared/"><![CDATA[<p><strong>The open source knowledge graph and GraphRAG databases worth evaluating in 2026 are <a href="https://arcadedb.com">ArcadeDB</a>, <a href="https://neo4j.com">Neo4j Community Edition</a>, <a href="https://memgraph.com">Memgraph</a>, <a href="https://falkordb.com">FalkorDB</a>, <a href="https://janusgraph.org">JanusGraph</a>, <a href="https://terminusdb.com">TerminusDB</a>, <a href="https://cayley.io">Cayley</a>, and <a href="https://github.com/kuzudb/kuzu">Kuzu</a>.</strong> Below we compare each on licensing, maintenance status, query languages, and whether it can serve GraphRAG retrieval, meaning graph traversal plus vector similarity, without a second database bolted alongside it.</p>

<p><strong>The short version:</strong> three of the eight index vectors in the same store as the graph, and can therefore serve GraphRAG on their own: ArcadeDB, Memgraph, and FalkorDB. They differ mainly on licence. ArcadeDB is Apache 2.0, Memgraph Community is BSL 1.1, FalkorDB is SSPLv1. Our own take on serving retrieval from one store is on the <a href="/graph-rag.html">Graph RAG</a> and <a href="/knowledge-graphs.html">knowledge graphs</a> pages. Only ArcadeDB is OSI open source, and only ArcadeDB also stores documents and time series and runs embedded.</p>

<p>A word on where this is published. This is the ArcadeDB blog, we build ArcadeDB, and we think it is the best fit for most knowledge graph projects in 2026. You should discount our opinion accordingly. What we can do is be precise about the things that are checkable: which licence each project uses, when it last shipped a release, and what it does not do. Every status claim in this article was verified against the project’s own repository on 30 July 2026, and we say plainly where an alternative is the better choice.</p>

<p>Two of the eight are not really live options any more, and we say so rather than padding the list.</p>

<hr />

<h2 id="what-makes-a-knowledge-graph-database">What Makes a Knowledge Graph Database?</h2>

<p>A knowledge graph is not just a graph. It is a graph where the nodes represent real entities (people, documents, products, concepts), the edges carry typed meaning (authored-by, cites, depends-on), and the whole thing is used to answer questions that require following those relationships rather than filtering a table.</p>

<p>That imposes requirements beyond “can store nodes and edges”:</p>

<ul>
  <li><strong>Multi-hop traversal at usable speed.</strong> The value of a knowledge graph is in the questions that cross three or four relationships. If those queries take minutes, nobody asks them.</li>
  <li><strong>Semantic retrieval.</strong> Users do not know your terminology. Someone searching for “how to handle errors in the payment service” needs to find a document titled “Exception Management in Billing Module”. That requires vector embeddings, not keyword matching.</li>
  <li><strong>Exact retrieval too.</strong> Sometimes the query is a specific error code or clause number, where semantic similarity is exactly wrong and you need literal full-text matching.</li>
  <li><strong>A licence you can actually build on.</strong> If you plan to embed the database in a product you distribute, a copyleft licence is a business decision, not a footnote.</li>
  <li><strong>Someone still maintaining it.</strong> A knowledge graph is long-lived infrastructure. Adopting an unmaintained engine means you have adopted its unfixed bugs permanently.</li>
</ul>

<p>That last criterion eliminates more candidates than people expect.</p>

<hr />

<h2 id="1-arcadedb">1. ArcadeDB</h2>

<p><strong>Licence:</strong> Apache 2.0 · <strong>Latest release:</strong> 26.7.3 (July 2026) · <strong>Status:</strong> actively developed</p>

<p>ArcadeDB is a multi-model database that stores graph, document, key-value, full-text search, vector, and time-series data in one engine, under a single ACID transaction boundary.</p>

<h3 id="why-it-stands-out">Why It Stands Out</h3>

<p>For knowledge graph work specifically, the argument is that a knowledge graph needs three retrieval modes and most stacks make you run three databases to get them.</p>

<p><strong>Graph, vectors, and full-text in one query.</strong> A realistic knowledge graph query does several things at once: find the entities semantically related to a question, filter to those matching an exact identifier, then traverse out to everything connected. On ArcadeDB that is one statement against one copy of the data. On most alternatives it is a graph query, a separate vector search, and an application layer merging the results, with the consistency between the two stores becoming your problem.</p>

<p><strong>Native vector search.</strong> ArcadeDB indexes embeddings with JVector, using DiskANN and HNSW with SIMD acceleration, over the same records that hold the graph. There is no plugin to install and no external service to keep in sync.</p>

<p><strong>Five query languages.</strong> SQL, OpenCypher 25, Apache TinkerPop Gremlin, GraphQL, and the MongoDB query language all run against the same data. The Cypher engine is native rather than a translation layer and passes 97.8% of the official TCK, which matters if you are moving existing queries across.</p>

<p><strong>Apache 2.0, with no edition split.</strong> Clustering with Raft consensus, replication, embedded mode, and vector search are all in the free build. There is no Enterprise edition holding back the features you need in production, and no node or core limit.</p>

<p><strong>Embedded or server.</strong> ArcadeDB runs inside your JVM, or in Python in-process via <code class="language-plaintext highlighter-rouge">pip install arcadedb-embedded</code> with a bundled JRE. For a knowledge graph feeding a local RAG pipeline, that removes the server entirely.</p>

<h3 id="where-its-not-the-best-fit">Where It’s Not the Best Fit</h3>

<p>Honesty requires a real list here, not a token one.</p>

<p><strong>It is the smallest community of the six.</strong> ArcadeDB has roughly 1,055 GitHub stars against Neo4j’s enormous ecosystem, Cayley’s 15,000, and JanusGraph’s 5,800. That translates into fewer Stack Overflow answers, fewer blog posts when you hit an edge case, and fewer engineers who already know it. If hiring people who have used your database before is a hard requirement, this is a genuine argument against us.</p>

<p><strong>It is not a triplestore.</strong> If your knowledge graph is RDF, your data is in Turtle or N-Triples, and your team writes SPARQL, ArcadeDB is the wrong shape. It is a property graph engine. Use a dedicated RDF store.</p>

<p><strong>It does not shard a single graph across machines.</strong> ArcadeDB replicates for availability rather than partitioning one graph across a cluster. If your graph genuinely exceeds what one machine can hold, JanusGraph over Cassandra is the more honest answer.</p>

<p><strong>It runs on the JVM.</strong> The bundled-JRE Python package hides this well, but the engine is Java. If your operational policy excludes the JVM, that is decisive.</p>

<hr />

<h2 id="2-neo4j-community-edition">2. Neo4j Community Edition</h2>

<p><strong>Licence:</strong> GPLv3 · <strong>Status:</strong> actively developed</p>

<p>Neo4j is the reference point for property graphs, and Community Edition is a genuinely capable database. It is also the option most often adopted without reading the licence.</p>

<h3 id="the-good">The Good</h3>

<p><strong>The ecosystem is unmatched.</strong> More documentation, more tutorials, more courses, more consultants, and more people who already know Cypher than every other option on this list combined. For a team learning graph modelling from scratch, that is worth a great deal.</p>

<p><strong>Cypher is excellent.</strong> Neo4j designed the language that the rest of the industry now implements. It is genuinely pleasant for pattern matching.</p>

<p><strong>Full ACID and a mature engine.</strong> Community Edition is not a crippled demo. For single-instance workloads it is a solid production database.</p>

<h3 id="the-problems">The Problems</h3>

<p><strong>GPLv3 is a business decision.</strong> Community Edition is copyleft. If you distribute software that includes or links to it, the licence reaches your application. For internal deployments this is usually irrelevant; for anything you ship to customers it is often a blocker, and the escape hatch is a commercial Enterprise licence.</p>

<p><strong>No clustering.</strong> Community Edition does not support clustering, so it is limited to single-instance deployments. High availability and read scaling are Enterprise features. For a knowledge graph that becomes load-bearing infrastructure, that ceiling arrives eventually.</p>

<p><strong>Graph only.</strong> Neo4j stores graphs. Documents, time series, and the rest live in other systems, which for knowledge graph work usually means running a separate store for the source documents alongside the graph of extracted entities.</p>

<h3 id="when-to-choose-it-anyway">When to Choose It Anyway</h3>

<p>If your team already knows Cypher, your deployment is internal and single-instance, and you value ecosystem depth over licence flexibility, Neo4j Community Edition is a reasonable and low-risk choice. Plenty of successful knowledge graphs run on it.</p>

<hr />

<h2 id="3-memgraph">3. Memgraph</h2>

<p><strong>Licence:</strong> BSL 1.1 (Community) · <strong>Latest release:</strong> v3.12.0 (July 2026) · <strong>Status:</strong> actively developed</p>

<!-- SOURCE licence: https://github.com/memgraph/memgraph/blob/master/licenses/BSL.txt — Community edition is BSL 1.1; Enterprise uses the separate MEL licence. -->
<!-- SOURCE release/stars: https://api.github.com/repos/memgraph/memgraph — v3.12.0 published 2026-07-15, 4,293 stars, not archived. Verified 30 July 2026. -->
<!-- SOURCE positioning + vector index: https://github.com/memgraph/memgraph repository description, "High-performance open-source in-memory graph database for GraphRAG, AI memory, agentic AI, and real-time graph analytics. Cypher-compatible, built in C++." -->

<p>Memgraph is an in-memory, Cypher-compatible graph database written in C++, and it has aimed squarely at GraphRAG and agent memory. If you have asked an AI assistant about GraphRAG recently, there is a good chance it cited Memgraph.</p>

<h3 id="the-good-1">The Good</h3>

<p><strong>Built for this workload.</strong> Memgraph ships text and vector indexes in the same store as the graph, so a retrieval pipeline can run graph traversal and similarity search as one database operation rather than two systems stitched together. This is the same architectural argument we make for ArcadeDB, and it is a fair one when they make it.</p>

<p><strong>In-memory speed.</strong> Holding the working set in memory gives Memgraph excellent latency on traversals, which matters when retrieval sits in the request path of an agent loop.</p>

<p><strong>Cypher compatible.</strong> Existing Cypher and Neo4j-shaped tooling largely carries over.</p>

<h3 id="the-problems-1">The Problems</h3>

<p><strong>BSL is not open source.</strong> Memgraph Community is licensed under the Business Source License 1.1, which the Open Source Initiative does not recognise as an open-source licence. It restricts commercial use in ways a permissive licence does not, and Enterprise sits behind a separate proprietary licence. If your reason for avoiding Neo4j was licensing, read the BSL carefully before treating Memgraph as the escape.</p>

<p><strong>In-memory is a cost model.</strong> RAM is the constraint. For a knowledge graph that is large and mostly cold, keeping it resident is a different budget from a disk-based engine.</p>

<p><strong>Graph plus vectors, not multi-model.</strong> Documents and time series live elsewhere.</p>

<h3 id="when-to-choose-it-anyway-1">When to Choose It Anyway</h3>

<p>If latency is your dominant constraint, your graph fits comfortably in memory, and BSL is acceptable to your legal team, Memgraph is genuinely strong at exactly this workload. It is the most direct competitor to ArcadeDB on GraphRAG and we would rather say so than pretend otherwise.</p>

<hr />

<h2 id="4-falkordb">4. FalkorDB</h2>

<p><strong>Licence:</strong> SSPLv1 · <strong>Latest release:</strong> v4.20.1 (July 2026) · <strong>Status:</strong> actively developed</p>

<!-- SOURCE licence: https://docs.falkordb.com/References/license.html — Server Side Public License v1 (SSPLv1). -->
<!-- SOURCE release/stars: https://api.github.com/repos/FalkorDB/FalkorDB — v4.20.1 published 2026-07-15, 4,853 stars, not archived. Verified 30 July 2026. -->
<!-- SOURCE GraphBLAS + positioning: https://github.com/FalkorDB/FalkorDB repository description, "uses GraphBLAS under the hood for its sparse adjacency matrix graph representation. Our goal is to provide the best Knowledge Graph for LLM (GraphRAG)." -->
<!-- SOURCE RedisGraph lineage: covered in our earlier post /blog/neo4j-alternatives-in-2026-a-fair-look-at-the-open-source-options/ -->

<p>FalkorDB is the continuation of RedisGraph after Redis discontinued it, rebuilt around GraphBLAS sparse adjacency matrices and aimed explicitly at knowledge graphs for LLMs.</p>

<h3 id="the-good-2">The Good</h3>

<p><strong>GraphBLAS is a genuinely good fit.</strong> Representing the graph as sparse matrices turns multi-hop traversal into linear algebra, and it makes FalkorDB fast on the pattern-matching workloads GraphRAG generates.</p>

<p><strong>HNSW vector index in the same store.</strong> Like Memgraph, FalkorDB indexes embeddings alongside the graph, so hybrid retrieval does not require a second database.</p>

<p><strong>Cypher, with a low-friction operational model.</strong> It runs as a Redis module, which is familiar territory for a lot of teams.</p>

<h3 id="the-problems-2">The Problems</h3>

<p><strong>SSPL is source-available, not open source.</strong> The Server Side Public License restricts how you may offer the software as a service and is not an OSI-approved open-source licence. For most self-hosted users this is not a practical constraint, but it is not the same freedom Apache 2.0 gives you, and it should not be described as open source.</p>

<p><strong>Graph and vectors only.</strong> Same limitation as Memgraph: no document or time-series model.</p>

<p><strong>Redis-module operational shape.</strong> Convenient if you already run Redis, an additional dependency if you do not.</p>

<h3 id="when-to-choose-it-anyway-2">When to Choose It Anyway</h3>

<p>If you want fast GraphRAG retrieval, you already operate Redis, and SSPL is acceptable, FalkorDB is a well-built option with a clear focus on exactly this use case.</p>

<hr />

<h2 id="5-janusgraph">5. JanusGraph</h2>

<p><strong>Licence:</strong> Apache 2.0 · <strong>Latest release:</strong> 1.1.0 (November 2024) · <strong>Status:</strong> maintained, slow release cadence</p>

<p>JanusGraph is a distributed graph database under the Linux Foundation, and the direct descendant of Titan. It is the option built for graphs that genuinely do not fit on one machine.</p>

<h3 id="the-good-3">The Good</h3>

<p><strong>Real horizontal scale.</strong> JanusGraph is a graph layer over a distributed storage backend, shipping support for Apache Cassandra, Apache HBase, and Oracle Berkeley DB Java Edition. If you already run Cassandra at scale, JanusGraph inherits that operational maturity and partitions a graph across it in a way most single-node engines cannot.</p>

<p><strong>Genuinely open, genuinely neutral.</strong> Apache 2.0, under the Linux Foundation, with no vendor holding an Enterprise edition back. There is no commercial upsell waiting.</p>

<p><strong>TinkerPop native.</strong> Gremlin is the query language, and JanusGraph is a first-class TinkerPop implementation, so the wider Gremlin tooling ecosystem works.</p>

<h3 id="the-problems-3">The Problems</h3>

<p><strong>You are operating at least two distributed systems.</strong> JanusGraph is not a complete database. Production means running and tuning Cassandra or HBase underneath it, plus usually Elasticsearch or Solr for indexing. The operational burden is substantially higher than any single-binary option here, and that is the dominant cost for most teams.</p>

<p><strong>Slow release cadence.</strong> Version 1.0.0 arrived in October 2023 and 1.1.0 in November 2024. The repository is active, but a project shipping roughly one release a year is a different maintenance proposition from one shipping monthly.</p>

<p><strong>Gremlin only, and no native vectors.</strong> There is no Cypher and no SQL. For semantic search you will pair it with a separate vector database, which for knowledge graph work means the multi-store architecture again.</p>

<h3 id="when-to-choose-it-anyway-3">When to Choose It Anyway</h3>

<p>If your graph is genuinely too large for one machine and you already operate Cassandra or HBase, JanusGraph is the right answer and we would tell you so. That is a real scenario and no amount of single-node performance changes it.</p>

<hr />

<h2 id="6-terminusdb">6. TerminusDB</h2>

<p><strong>Licence:</strong> Apache 2.0 · <strong>Latest release:</strong> 12.0.6 (June 2026) · <strong>Status:</strong> actively developed</p>

<p>TerminusDB is a document graph database whose distinguishing idea is git-style version control for data: branching, merging, diffing, and time travel over the database itself.</p>

<h3 id="the-good-4">The Good</h3>

<p><strong>Versioning is a first-class feature, not a pattern you implement.</strong> For a knowledge graph curated by humans, being able to branch the graph, make changes, review a diff, and merge is genuinely valuable and nothing else on this list offers it natively. Regulatory and scientific use cases where you must prove what the data said last quarter are a natural fit.</p>

<p><strong>Apache 2.0 and actively shipping.</strong> Releases are current, the repository saw commits the week this was written, and the licence is permissive.</p>

<p><strong>Document plus graph model.</strong> Data is modelled as documents with a schema, connected as a graph, which fits knowledge graph work better than a pure triplestore for most application developers.</p>

<h3 id="the-problems-4">The Problems</h3>

<p><strong>Smaller ecosystem than its age suggests.</strong> Roughly 3,370 GitHub stars and a modest contributor base. Documentation and community answers are thinner than Neo4j or JanusGraph.</p>

<p><strong>No native vector search.</strong> TerminusDB does not index embeddings alongside the graph, so semantic retrieval means a separate vector store. <!-- UNVERIFIED: TerminusDB may have added vector capability in a recent release; verify against current docs before publishing. --></p>

<p><strong>WOQL is a learning curve.</strong> The native query language is its own thing rather than Cypher, SQL, or Gremlin, so existing team knowledge does not transfer.</p>

<h3 id="when-to-choose-it-anyway-4">When to Choose It Anyway</h3>

<p>If versioning and provenance are the primary requirement rather than a nice-to-have, TerminusDB is the strongest option here and the comparison is not close.</p>

<hr />

<h2 id="7-cayley">7. Cayley</h2>

<p><strong>Licence:</strong> Apache 2.0 · <strong>Latest tagged release:</strong> v0.7.7 (October 2019) · <strong>Status:</strong> effectively dormant</p>

<p>Cayley is a linked-data graph database written in Go, inspired by the graph infrastructure behind Google’s Knowledge Graph. It has 15,045 GitHub stars, which is more than anything else on this list, and it is the clearest illustration of why star counts are a poor proxy for project health.</p>

<h3 id="the-good-5">The Good</h3>

<p><strong>The design is genuinely nice.</strong> A Go binary with pluggable backends, a clean HTTP API, and a small footprint. For a read-mostly linked-data store it is pleasant to work with.</p>

<p><strong>Permissively licensed with no vendor.</strong> Apache 2.0, no commercial edition, no strings.</p>

<h3 id="the-problems-5">The Problems</h3>

<p><strong>No tagged release since October 2019.</strong> That is the single fact that matters. The repository is not archived and there is occasional commit activity, but a database that has not cut a release in nearly seven years is not something to build a knowledge graph on in 2026. Bug fixes, security patches, and dependency updates are not arriving on any schedule you can rely on.</p>

<p><strong>No vector search, no modern AI integration.</strong> Cayley predates the entire embedding era and has no answer for semantic retrieval.</p>

<p><strong>Query languages are its own.</strong> Gizmo, a JavaScript-based query API, alongside GraphQL and MQL variants. <!-- UNVERIFIED: exact set of query languages in current Cayley; verify against project docs before publishing. --></p>

<h3 id="verdict">Verdict</h3>

<p>We would not start a new knowledge graph on Cayley in 2026, and we would say the same if it were our own project. Included here because its star count keeps it near the top of search results, and readers deserve to know that the number reflects 2015 enthusiasm rather than 2026 maintenance.</p>

<hr />

<h2 id="8-kuzu">8. Kuzu</h2>

<p><strong>Licence:</strong> MIT · <strong>Latest release:</strong> v0.11.3 (October 2025) · <strong>Status:</strong> archived</p>

<p>Kuzu was an embedded analytical graph database with a columnar storage engine and a Cypher interface, and it was genuinely good at what it did.</p>

<h3 id="what-happened">What Happened</h3>

<p>The GitHub repository was archived in October 2025 following the team’s acquisition by Apple. Development stopped. The code remains available under MIT, and community forks exist, but there is no funded maintainer and no release schedule.</p>

<h3 id="strengths-while-it-lasted">Strengths, While It Lasted</h3>

<p><strong>Excellent analytical performance.</strong> Kuzu’s columnar engine was fast on multi-hop analytical queries, and it still appears in benchmark comparisons, including <a href="https://arcadedb.com/benchmarks.html">our own LDBC results</a>, where it is marginally faster than ArcadeDB on LSQB Q2. We are not going to pretend otherwise.</p>

<p><strong>Embedded and Python-first.</strong> It brought embedded graph workloads to Python properly, which is exactly the niche <code class="language-plaintext highlighter-rouge">arcadedb-embedded</code> now occupies.</p>

<h3 id="limitations">Limitations</h3>

<p><strong>It is archived.</strong> Everything else is secondary. An archived database is not a foundation for new infrastructure, however good the engine was.</p>

<p><strong>MIT means the code survives.</strong> If you already run Kuzu, you are not stranded, and forking is legally straightforward. But you now own a database engine, which is a larger commitment than most teams intend to make.</p>

<hr />

<h2 id="the-comparison-at-a-glance">The Comparison at a Glance</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>ArcadeDB</th>
      <th>Neo4j CE</th>
      <th>Memgraph</th>
      <th>FalkorDB</th>
      <th>JanusGraph</th>
      <th>TerminusDB</th>
      <th>Cayley</th>
      <th>Kuzu</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Licence</strong></td>
      <td>Apache 2.0</td>
      <td>GPLv3</td>
      <td>BSL 1.1</td>
      <td>SSPLv1</td>
      <td>Apache 2.0</td>
      <td>Apache 2.0</td>
      <td>Apache 2.0</td>
      <td>MIT</td>
    </tr>
    <tr>
      <td><strong>OSI open source</strong></td>
      <td>Yes</td>
      <td>Yes</td>
      <td>No</td>
      <td>No</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td><strong>Status (Jul 2026)</strong></td>
      <td>Active</td>
      <td>Active</td>
      <td>Active</td>
      <td>Active</td>
      <td>Maintained</td>
      <td>Active</td>
      <td>Dormant</td>
      <td>Archived</td>
    </tr>
    <tr>
      <td><strong>Latest release</strong></td>
      <td>26.7.3</td>
      <td>Current</td>
      <td>v3.12.0</td>
      <td>v4.20.1</td>
      <td>1.1.0 (2024)</td>
      <td>12.0.6</td>
      <td>v0.7.7 (2019)</td>
      <td>v0.11.3 (2025)</td>
    </tr>
    <tr>
      <td><strong>Vectors in-store</strong></td>
      <td>Yes</td>
      <td>Partial</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
    </tr>
    <tr>
      <td><strong>Serves GraphRAG alone</strong></td>
      <td>Yes</td>
      <td>Partial</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
    </tr>
    <tr>
      <td><strong>Beyond graph</strong></td>
      <td>Doc, KV, TS, FTS</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>Doc</td>
      <td>No</td>
      <td>No</td>
    </tr>
    <tr>
      <td><strong>Clustering free</strong></td>
      <td>Yes</td>
      <td>No</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>Yes</td>
      <td>n/a</td>
      <td>n/a</td>
    </tr>
    <tr>
      <td><strong>Query languages</strong></td>
      <td>5</td>
      <td>Cypher</td>
      <td>Cypher</td>
      <td>Cypher</td>
      <td>Gremlin</td>
      <td>WOQL</td>
      <td>Gizmo</td>
      <td>Cypher</td>
    </tr>
    <tr>
      <td><strong>Embedded mode</strong></td>
      <td>Yes</td>
      <td>No (Enterprise)</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>Yes</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td><strong>Separate storage backend</strong></td>
      <td>No</td>
      <td>No</td>
      <td>No</td>
      <td>Redis module</td>
      <td>Required</td>
      <td>No</td>
      <td>Optional</td>
      <td>No</td>
    </tr>
    <tr>
      <td><strong>GitHub stars</strong></td>
      <td>1,055</td>
      <td>Very large</td>
      <td>4,293</td>
      <td>4,853</td>
      <td>5,816</td>
      <td>3,370</td>
      <td>15,045</td>
      <td>4,028</td>
    </tr>
  </tbody>
</table>

<p>Star counts and release data verified against each project’s GitHub repository on 30 July 2026.</p>

<hr />

<h2 id="why-licensing-matters-for-knowledge-graphs">Why Licensing Matters for Knowledge Graphs</h2>

<p>Knowledge graphs have a licensing problem that other databases do not, because they tend to end up embedded in products rather than sitting behind a service boundary.</p>

<p>A knowledge graph that powers an internal search tool is a service you deploy, and GPLv3 is largely irrelevant. A knowledge graph that ships inside a desktop application, an on-premise product, or a customer-installed agent is distributed software, and a copyleft licence reaches your code. Teams frequently discover this after the architecture is settled.</p>

<p>Apache 2.0, used by ArcadeDB, JanusGraph, TerminusDB, and Cayley, permits embedding in proprietary software with no source obligation. MIT, used by Kuzu, is similarly permissive. GPLv3, used by Neo4j Community Edition, does not, and the commercial escape hatch is priced accordingly.</p>

<p>The second licensing question is what the free edition withholds. Neo4j reserves clustering and embedded mode for Enterprise. ArcadeDB, JanusGraph, and TerminusDB do not have a paid edition holding features back at all.</p>

<hr />

<h2 id="so-which-one-should-you-choose">So Which One Should You Choose?</h2>

<p><strong>Choose ArcadeDB</strong> if you want graph traversal, vector search, and full-text retrieval in one engine under a permissive licence, and you would rather not operate three databases to build one knowledge graph. Accept that you are picking the smallest community here.</p>

<p><strong>Choose Neo4j Community Edition</strong> if ecosystem depth matters more than licence flexibility, your deployment is internal and single-instance, and your team already writes Cypher.</p>

<p><strong>Choose Memgraph</strong> if retrieval latency is the dominant constraint, your graph fits in memory, and your legal team is comfortable with BSL 1.1.</p>

<p><strong>Choose FalkorDB</strong> if you want fast GraphRAG retrieval, already operate Redis, and SSPL is acceptable.</p>

<p><strong>Choose JanusGraph</strong> if your graph genuinely does not fit on one machine and you already operate Cassandra or HBase. Do not choose it to avoid that operational burden, because it adds to it.</p>

<p><strong>Choose TerminusDB</strong> if data versioning, branching, and provenance are core requirements rather than conveniences.</p>

<p><strong>Do not start on Cayley</strong> in 2026, despite the star count, unless you are prepared to maintain it.</p>

<p><strong>Do not start on Kuzu</strong>, because it is archived, unless you are deliberately adopting a fork and accepting ownership.</p>

<hr />

<h2 id="key-takeaways">Key Takeaways</h2>

<ul>
  <li>Six of these eight are actively maintained. Cayley has not cut a release since 2019, and Kuzu was archived in October 2025.</li>
  <li>GitHub stars measure historical enthusiasm, not current health. Cayley has the most stars and the least maintenance.</li>
  <li>Licensing determines whether you can embed the database in a distributed product. Apache 2.0 and MIT permit it; GPLv3 constrains it.</li>
  <li>GraphRAG needs graph traversal and vector similarity together. Three of these eight do both in one store: ArcadeDB, Memgraph, and FalkorDB. The other five expect a separate vector database.</li>
  <li>The GraphRAG shortlist comes down to licence: ArcadeDB is Apache 2.0, Memgraph is BSL, FalkorDB is SSPL. Only ArcadeDB is OSI open source, and only ArcadeDB is multi-model and embeddable.</li>
  <li>ArcadeDB’s genuine weakness is community size: it has the fewest GitHub stars of the eight.</li>
</ul>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<h3 id="what-is-the-best-open-source-knowledge-graph-database-in-2026">What is the best open source knowledge graph database in 2026?</h3>

<p>It depends on what you already run. ArcadeDB, Memgraph, and FalkorDB all index vectors alongside the graph and can serve GraphRAG alone; they differ on licence, with only ArcadeDB being OSI open source. JanusGraph is better at scale on Cassandra or HBase, and TerminusDB is better when versioning matters most.</p>

<h3 id="which-open-source-database-is-best-for-graphrag">Which open source database is best for GraphRAG?</h3>

<p>GraphRAG needs graph traversal and vector similarity in the same query. ArcadeDB, Memgraph, and FalkorDB all index embeddings in the same store as the graph, so any of the three can serve GraphRAG without a separate vector database. ArcadeDB is the only one of the three under an OSI-approved open source licence.</p>

<h3 id="is-neo4j-community-edition-suitable-for-a-production-knowledge-graph">Is Neo4j Community Edition suitable for a production knowledge graph?</h3>

<p>It can be, with two caveats. Community Edition is GPLv3, which is a copyleft licence that affects how you can distribute software built on it, and it does not support clustering, so it is limited to single-instance deployments. Both are Enterprise Edition features.</p>

<h3 id="which-open-source-knowledge-graph-databases-are-still-actively-maintained">Which open source knowledge graph databases are still actively maintained?</h3>

<p>As of July 2026, ArcadeDB, Memgraph, FalkorDB, TerminusDB, JanusGraph, and Neo4j all ship releases. Kuzu was archived on GitHub in October 2025 after its team was acquired by Apple. Cayley’s repository is not archived but its last tagged release, v0.7.7, dates from October 2019.</p>

<h3 id="do-i-need-a-separate-vector-database-for-a-knowledge-graph">Do I need a separate vector database for a knowledge graph?</h3>

<p>Not necessarily. A knowledge graph that also serves semantic search needs both relationships and embeddings. ArcadeDB indexes vectors natively with JVector alongside the graph, so a single query can rank by similarity and traverse edges. Most alternatives require pairing the graph with a separate vector store.</p>

<h3 id="what-is-the-difference-between-a-knowledge-graph-and-an-rdf-triplestore">What is the difference between a knowledge graph and an RDF triplestore?</h3>

<p>A triplestore models data strictly as subject-predicate-object triples and typically queries with SPARQL. A property graph attaches arbitrary properties to nodes and edges and queries with Cypher, Gremlin, or SQL. Both can express a knowledge graph; property graphs are usually easier for application developers.</p>

<h3 id="does-janusgraph-need-cassandra-or-hbase-to-run">Does JanusGraph need Cassandra or HBase to run?</h3>

<p>JanusGraph is a graph layer rather than a complete storage engine, so it runs on top of a separate backend. It ships support for Apache Cassandra, Apache HBase, and Oracle Berkeley DB Java Edition. Berkeley DB suits local development; Cassandra or HBase is expected in production.</p>

<hr />

<h2 id="getting-started-with-arcadedb">Getting Started with ArcadeDB</h2>

<p>If the single-engine argument is the one that lands, the fastest way to test it is to build a small knowledge graph on your own data and see whether one query really can do all three retrieval modes.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-p</span> 2480:2480 <span class="nt">-p</span> 2424:2424 <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">JAVA_OPTS</span><span class="o">=</span><span class="s2">"-Darcadedb.server.rootPassword=playwithdata"</span> <span class="se">\</span>
  arcadedata/arcadedb:latest
</code></pre></div></div>

<p>Then open ArcadeDB Studio at <code class="language-plaintext highlighter-rouge">http://localhost:2480</code> and start modelling. For the concepts behind entity extraction, semantic search, and temporal knowledge, see <a href="https://arcadedb.com/knowledge-graphs.html">knowledge graphs on ArcadeDB</a>. If you are building retrieval for an LLM on top of it, <a href="https://arcadedb.com/graph-rag.html">GraphRAG</a> covers the hybrid retrieval patterns. If you are migrating an existing graph, the <a href="https://arcadedb.com/neo4j.html">Neo4j migration guide</a> covers Cypher and Bolt compatibility.</p>

<p>ArcadeDB is Apache 2.0, free in production, with no node limits and no Enterprise edition. Benchmark it against whatever you run today and keep whichever wins.</p>]]></content><author><name>Luca Garulli</name></author><category term="Knowledge Graph" /><category term="GraphRAG" /><category term="Graph Database" /><category term="Open Source" /><category term="Comparison" /><category term="Multi-Model" /><category term="Memgraph" /><category term="FalkorDB" /><category term="JanusGraph" /><category term="TerminusDB" /><category term="Cayley" /><category term="Kuzu" /><category term="Neo4j" /><category term="Knowledge Graph Database" /><summary type="html"><![CDATA[Open source knowledge graph and GraphRAG databases compared in 2026: ArcadeDB, Neo4j CE, Memgraph, FalkorDB, JanusGraph, TerminusDB, Cayley, and Kuzu, on licensing, vector search, and maintenance.]]></summary></entry><entry><title type="html">ArcadeDB 26.7.3: Three Security Advisories and a Super-Node Edge-Merge Data-Loss Fix</title><link href="https://arcadedb.com/blog/arcadedb-26-7-3/" rel="alternate" type="text/html" title="ArcadeDB 26.7.3: Three Security Advisories and a Super-Node Edge-Merge Data-Loss Fix" /><published>2026-07-17T00:00:00+00:00</published><updated>2026-07-17T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-26-7-3</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-26-7-3/"><![CDATA[<p><strong>ArcadeDB 26.7.3</strong> is a hotfix on top of <a href="https://arcadedb.com/blog/arcadedb-26-7-2/">26.7.2</a>. It closes three security advisories, two in the MCP server transport and one in the JavaScript/Java trigger authorization gate, and repairs a data-loss regression in the commutative super-node edge-append merge that 26.7.2 introduced.</p>

<p>Upgrade if you can. It matters most if you expose the MCP server, run in server or multi-tenant mode, or store high-degree (super-node) graphs. There are no breaking changes and no schema migration in this release.</p>

<h2 id="major-highlights">Major Highlights</h2>

<h3 id="security-advisories">Security Advisories</h3>

<p>All three come from the same internal audit that produced the 26.7.2 fixes, covering MCP-transport and trigger surfaces that round did not reach.</p>

<ul>
  <li><strong>MCP command transport disabled all engine permission checks</strong> (<a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-6x73-v3rc-f57c">GHSA-6x73-v3rc-f57c</a>). The MCP transport never bound the authenticated principal onto the request thread’s <code class="language-plaintext highlighter-rouge">DatabaseContext</code>, so the engine permission gates (which are deliberate no-ops when no user is bound) silently passed for every MCP caller. A non-root, MCP-allowed reader could perform arbitrary writes, DDL, and schema or security mutation; the <code class="language-plaintext highlighter-rouge">query</code> + <code class="language-plaintext highlighter-rouge">js</code> sub-case could execute arbitrary in-JVM JavaScript. The principal is now bound at the single DB-resolution chokepoint and cleared on the pooled worker thread, so the engine per-user gates enforce for MCP exactly as they do for the HTTP, Bolt, PostgreSQL, and gRPC transports.</li>
  <li><strong>MCP <code class="language-plaintext highlighter-rouge">get_server_settings</code> leaked the HA clusterToken in cleartext</strong> (<a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-p9wc-4fhr-78wm">GHSA-p9wc-4fhr-78wm</a>). The MCP <code class="language-plaintext highlighter-rouge">get_server_settings</code> tool masked only settings whose key contained <code class="language-plaintext highlighter-rouge">"password"</code>, so <code class="language-plaintext highlighter-rouge">arcadedb.ha.clusterToken</code> was returned raw. That token is the trust anchor for cluster-forwarded authentication, so leaking it enables full root impersonation. This is the MCP sibling of the 26.7.2 fix (<a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-46hj-24h4-j8gf">GHSA-46hj-24h4-j8gf</a>); the tool now redacts both value and default via <code class="language-plaintext highlighter-rouge">GlobalConfiguration.isHidden()</code>, matching <code class="language-plaintext highlighter-rouge">GetServerHandler</code>.</li>
  <li><strong>JavaScript / Java triggers could escalate a schema admin to server-wide admin</strong> (<a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-38pf-6hp2-pxww">GHSA-38pf-6hp2-pxww</a>). A <code class="language-plaintext highlighter-rouge">JAVASCRIPT</code> trigger binds the real database object into a GraalVM context, so its script could call <code class="language-plaintext highlighter-rouge">database.getSecurity().createUser(...)</code> and escalate an <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code> (schema-admin) user to a server-wide admin; a <code class="language-plaintext highlighter-rouge">JAVA</code> trigger runs an arbitrary loaded class. Creating either host-code trigger type now requires <code class="language-plaintext highlighter-rouge">UPDATE_SECURITY</code> at the <code class="language-plaintext highlighter-rouge">LocalSchema.createTrigger</code> chokepoint, mirroring the <code class="language-plaintext highlighter-rouge">DEFINE FUNCTION ... LANGUAGE js</code> gate (<a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-vwjc-v7x7-cm6g">GHSA-vwjc-v7x7-cm6g</a>). Declarative SQL triggers keep <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code>, and schema reload of existing triggers is unaffected.</li>
</ul>

<h2 id="major-fixes">Major Fixes</h2>

<h3 id="graph-engine">Graph Engine</h3>

<ul>
  <li>
    <p><strong>Edge-append merge no longer reverts concurrent writes on multi-page edge chunks</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/5302">#5302</a>). The commutative edge-append merge (<code class="language-plaintext highlighter-rouge">GRAPH_EDGE_APPEND_MERGE</code>, introduced in 26.7.2) resolved a commit-time page conflict by re-deriving the conflicted page and replaying the transaction’s tracked appends. For an edge chunk stored as a <strong>multi-page record</strong> (a chunk that straddles a page boundary) this re-derivation was unsound: the rebase re-read the chunk through the transaction’s stale in-transaction page copy and committed it, silently reverting concurrently committed appends on the continuation page. The observable symptoms were zeroed chunk tails, shifted or aliased pairs, lost edges, and <code class="language-plaintext highlighter-rouge">BufferUnderflowException</code> on later traversals of the vertex.</p>

    <p>Only records living entirely in place on the conflicted page are now re-derived. Multi-page and indirected (placeholder) chunk records fall back to the standard full-transaction retry. Single-page chunks, which are the vast majority and include all super-node stripe chunks, keep the merge.</p>
  </li>
</ul>

<h3 id="mcp-server">MCP Server</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">get_schema</code> now builds its schema through a dedicated <code class="language-plaintext highlighter-rouge">buildSchema</code> path.</li>
  <li>The MCP dispatcher handles a missing resource with a proper <code class="language-plaintext highlighter-rouge">MCPResourceNotFoundException</code> instead of a generic failure.</li>
</ul>

<h2 id="getting-started-with-2673">Getting Started with 26.7.3</h2>

<h3 id="docker">Docker</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull arcadedata/arcadedb:26.7.3
</code></pre></div></div>

<p>Visit our <a href="https://hub.docker.com/r/arcadedata/arcadedb">Docker Hub repository</a> for more information.</p>

<h3 id="maven">Maven</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>com.arcadedb<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>arcadedb-engine<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;version&gt;</span>26.7.3<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>All artifacts are available on <a href="https://repo.maven.apache.org/maven2/com/arcadedb/">Maven Central</a>.</p>

<h3 id="documentation">Documentation</h3>

<p>For details on features and usage, see the <a href="https://docs.arcadedb.com/">documentation</a>.</p>

<h2 id="compatibility-note">Compatibility Note</h2>

<p>This release contains no breaking changes and no schema migration. It maintains 100% compatibility with previous database formats, meaning no export/import is required when upgrading. As always, we recommend creating a database backup before upgrading.</p>

<hr />

<p><strong>Download ArcadeDB 26.7.3 now</strong>: <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.7.3">GitHub Releases</a></p>

<p>Thanks to everyone in the community who reported issues, opened PRs, and helped shape this release.</p>

<p>Luca Garulli
ArcadeDB Founder</p>]]></content><author><name>Luca Garulli</name></author><category term="Multi-Model" /><category term="Security" /><category term="MCP" /><category term="Graph Database" /><category term="Release" /><summary type="html"><![CDATA[ArcadeDB 26.7.3 is a focused hotfix on top of 26.7.2: it closes three security advisories (two in the MCP server transport, one in the JavaScript/Java trigger authorization gate) and repairs a data-loss regression in the commutative super-node edge-append merge.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/release-v26.7.3.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/release-v26.7.3.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Certified, Not Claimed: ArcadeDB’s Bolt Compatibility With Every Official Neo4j Driver</title><link href="https://arcadedb.com/blog/bolt-driver-compatibility-certification/" rel="alternate" type="text/html" title="Certified, Not Claimed: ArcadeDB’s Bolt Compatibility With Every Official Neo4j Driver" /><published>2026-07-14T00:00:00+00:00</published><updated>2026-07-14T00:00:00+00:00</updated><id>https://arcadedb.com/blog/bolt-driver-compatibility-certification</id><content type="html" xml:base="https://arcadedb.com/blog/bolt-driver-compatibility-certification/"><![CDATA[<p>Plenty of databases say they “support the Bolt protocol.” Almost none of them tell you what that sentence leaves out.</p>

<p>ArcadeDB has spoken Bolt for a while, and the honest version of our claim was narrow: the official Neo4j drivers connect, and simple queries work. Useful, but not the same thing as compatibility. The gap between the two is where users get hurt. The driver connects on Monday; on Thursday someone reads a <code class="language-plaintext highlighter-rouge">datetime</code> back out of a query and gets a string.</p>

<p><a href="/blog/arcadedb-26-7-2/">ArcadeDB 26.7.2</a> closes that gap. It ships a shared conformance spec, all five official drivers under test, fixes for the protocol bugs that testing exposed, and a compatibility matrix that regenerates itself every night and is published where anyone can read it.</p>

<h2 id="the-audit">The audit</h2>

<p>We started by auditing what we actually had (<a href="https://github.com/ArcadeData/arcadedb/issues/4882">epic #4882</a>). Some of it was reassuring. The full Bolt 3.0/4.0/4.4 message set was implemented, <code class="language-plaintext highlighter-rouge">ROUTE</code> included, along with a hand-written PackStream encoder, correct structure tags for Node, Relationship, and Path, TLS via <code class="language-plaintext highlighter-rouge">bolt+s</code>, and Neo4j-style structured error codes.</p>

<p>The rest was not:</p>

<ul>
  <li><strong>Bolt 5.x was never advertised.</strong> The server negotiated 3.0, 4.0, and 4.4. Modern 5.x drivers worked only by silently downgrading, which nobody had documented or tested as a deliberate stance.</li>
  <li><strong>Temporal values went out as strings.</strong> <code class="language-plaintext highlighter-rouge">Date</code>, <code class="language-plaintext highlighter-rouge">Time</code>, <code class="language-plaintext highlighter-rouge">LocalDateTime</code>, <code class="language-plaintext highlighter-rouge">DateTime</code> and friends were serialized as ISO-8601 text instead of native Bolt structures. <code class="language-plaintext highlighter-rouge">Duration</code> and spatial <code class="language-plaintext highlighter-rouge">Point</code> had no handling anywhere. The driver dutifully handed your application a <code class="language-plaintext highlighter-rouge">String</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Neo.TransientError.*</code> did not exist.</strong> Only a handful of <code class="language-plaintext highlighter-rouge">ClientError</code> and <code class="language-plaintext highlighter-rouge">DatabaseError</code> codes were defined, so the drivers’ managed-transaction retry logic could never fire the way it does against Neo4j.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">ROUTE</code> was single-node only.</strong> It returned this node’s own address as writer, reader, and router, so <code class="language-plaintext highlighter-rouge">neo4j://</code> routing against a real HA cluster was unproven.</li>
  <li><strong>Three of the five official drivers had zero Bolt coverage.</strong> The Python and C# e2e suites tested the Postgres wire protocol and HTTP. There was no Go module at all.</li>
  <li><strong>Java and JavaScript coverage was shallow.</strong> Connect, run a query, one regression test. No transactions, no error paths, no type round-trips.</li>
</ul>

<p>A compatibility claim you cannot fail is not a compatibility claim. That list is why the epic existed.</p>

<h2 id="the-rules-we-set">The rules we set</h2>

<p><strong>Certify depth, not presence.</strong> “The driver connects” is not certification. Every driver runs the full feature matrix, and every unsupported cell becomes a documented limitation instead of a silent omission.</p>

<p><strong>Only the real drivers.</strong> <code class="language-plaintext highlighter-rouge">neo4j-java-driver</code>, <code class="language-plaintext highlighter-rouge">neo4j-driver</code>, <code class="language-plaintext highlighter-rouge">neo4j</code>, <code class="language-plaintext highlighter-rouge">Neo4j.Driver</code>, <code class="language-plaintext highlighter-rouge">neo4j-go-driver</code>. No mocks, no bespoke socket clients. If the driver your application imports cannot do it, we do not get to claim it.</p>

<p><strong>One spec, five idiomatic suites.</strong> We deliberately did not build a YAML-driven test runner in five languages. The <a href="https://github.com/ArcadeData/arcadedb/blob/main/bolt/conformance/spec.yaml">conformance spec</a> is a reference document. Each scenario is hand-written into that language’s native framework (JUnit, jest, pytest, xUnit, <code class="language-plaintext highlighter-rouge">go test</code>) and tagged with the scenario ID. The spec owns <em>what</em> gets tested; the code stays polyglot and readable.</p>

<p><strong>“Not supported” is an acceptable answer, as long as it is written down.</strong> Byte-for-byte parity with Neo4j server behavior was never the goal. Knowing exactly where we differ was worth more.</p>

<h2 id="the-spec-39-scenarios-9-areas">The spec: 39 scenarios, 9 areas</h2>

<p>The matrix is defined once, in <code class="language-plaintext highlighter-rouge">bolt/conformance/spec.yaml</code>, across nine areas taken verbatim from the epic’s feature table:</p>

<table>
  <thead>
    <tr>
      <th>Area</th>
      <th>What it pins down</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">connection</code></td>
      <td><code class="language-plaintext highlighter-rouge">bolt://</code>, <code class="language-plaintext highlighter-rouge">bolt+s://</code> with TLS required and optional, <code class="language-plaintext highlighter-rouge">neo4j://</code> routing discovery</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">auth</code></td>
      <td>Basic auth success and failure; the <code class="language-plaintext highlighter-rouge">none</code> scheme being rejected (intentional, now certified as such)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">transactions</code></td>
      <td>Autocommit, explicit BEGIN/COMMIT/ROLLBACK, managed transaction functions, retry on transient errors</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">causal-consistency</code></td>
      <td>Bookmarks enforcing read-after-write across sessions</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">multi-database</code></td>
      <td>Session database selection and isolation between databases on one driver</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">result-handling</code></td>
      <td>Streaming <code class="language-plaintext highlighter-rouge">PULL</code>, <code class="language-plaintext highlighter-rouge">PULL n</code> resumption, <code class="language-plaintext highlighter-rouge">DISCARD</code>, <code class="language-plaintext highlighter-rouge">ResultSummary</code> counters</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">type-roundtrip</code></td>
      <td>Node, Relationship, Path, ByteArray, nested collections, nulls, all five temporal types, Duration, Point</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">errors</code></td>
      <td><code class="language-plaintext highlighter-rouge">Neo.ClientError.*</code> and <code class="language-plaintext highlighter-rouge">Neo.TransientError.*</code> so driver retry behavior matches Neo4j</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">protocol</code></td>
      <td>3.0/4.0/4.4 negotiation, 5.x negotiation, <code class="language-plaintext highlighter-rouge">RESET</code> mid-stream</td>
    </tr>
  </tbody>
</table>

<p>Each scenario carries a stable ID (<code class="language-plaintext highlighter-rouge">TYPE-011</code>, <code class="language-plaintext highlighter-rouge">PROTO-002</code>), a fixture, given/when/then steps, and a status. Every test in every language embeds its scenario ID in the test name, so checking coverage is a grep.</p>

<h2 id="the-drivers-14-pinned-versions">The drivers: 14 pinned versions</h2>

<p>Testing against <code class="language-plaintext highlighter-rouge">latest</code> tells you about today and nothing about tomorrow. Every language is pinned to a band set, resolved to concrete versions in <a href="https://github.com/ArcadeData/arcadedb/blob/main/bolt/conformance/driver-versions.md"><code class="language-plaintext highlighter-rouge">driver-versions.md</code></a>:</p>

<table>
  <thead>
    <tr>
      <th>Driver</th>
      <th>Versions under test</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Java (<code class="language-plaintext highlighter-rouge">neo4j-java-driver</code>)</td>
      <td>4.4.20, 5.28.5, 6.2.0</td>
    </tr>
    <tr>
      <td>JavaScript (<code class="language-plaintext highlighter-rouge">neo4j-driver</code>)</td>
      <td>5.28.3, 6.2.0</td>
    </tr>
    <tr>
      <td>Python (<code class="language-plaintext highlighter-rouge">neo4j</code>)</td>
      <td>5.28.4, 6.1.0, 6.2.0</td>
    </tr>
    <tr>
      <td>.NET (<code class="language-plaintext highlighter-rouge">Neo4j.Driver</code>)</td>
      <td>5.26.2, 5.28.4, 6.2.1</td>
    </tr>
    <tr>
      <td>Go (<code class="language-plaintext highlighter-rouge">neo4j-go-driver</code>)</td>
      <td>5.27.0, 5.28.0, 5.28.4</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">latest</code> band deliberately tracks the newest release, so a driver-side release that breaks compatibility trips our nightly run within a day of shipping.</p>

<p>Two of these bands carry compromises. The C# floor is 5.26.2 instead of a 4.x line, because <code class="language-plaintext highlighter-rouge">Neo4j.Driver</code> made breaking API changes in 5.0 that the shared suite cannot compile against. JavaScript has no 4.x band for the same reason. The 4.4 <em>wire protocol</em> is still covered through the Java 4.4.20 legacy driver. Both compromises are written down in the file, with the reasoning, so nobody has to reverse-engineer why a column is missing.</p>

<h2 id="what-the-tests-broke">What the tests broke</h2>

<p>The first full run lit up red, which was more or less the point. Everything from the audit list is now fixed in 26.7.2:</p>

<ul>
  <li><strong>Native temporal structures.</strong> <code class="language-plaintext highlighter-rouge">Date</code>, <code class="language-plaintext highlighter-rouge">Time</code>, <code class="language-plaintext highlighter-rouge">LocalTime</code>, <code class="language-plaintext highlighter-rouge">LocalDateTime</code>, and <code class="language-plaintext highlighter-rouge">DateTime</code> are emitted as native PackStream structures.</li>
  <li><strong>Duration and Point.</strong> Both round-trip as native Bolt structures in both directions, bound parameters included.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Neo.TransientError.*</code>.</strong> Retryable conflicts now map to transient error codes, so the drivers’ <code class="language-plaintext highlighter-rouge">executeWrite</code> retry loop works out of the box instead of failing your write on the first optimistic-lock conflict.</li>
  <li><strong>HA-aware <code class="language-plaintext highlighter-rouge">ROUTE</code>.</strong> <code class="language-plaintext highlighter-rouge">neo4j://</code> returns the real cluster topology instead of the local node three times.</li>
  <li><strong>Bolt 5.x negotiation.</strong> No more silent downgrade.</li>
  <li><strong>Populated write counters.</strong> <code class="language-plaintext highlighter-rouge">ResultSummary</code> counters reflect what actually happened.</li>
</ul>

<h3 id="one-breaking-change-to-plan-for">One breaking change to plan for</h3>

<p>The temporal fix changes behavior, and it is the one thing to read before you upgrade. Values that used to arrive as strings now arrive as typed objects:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Before 26.7.2: this returned an ISO-8601 String</span>
<span class="nc">String</span> <span class="n">when</span> <span class="o">=</span> <span class="kd">record</span><span class="err">.</span><span class="nc">get</span><span class="o">(</span><span class="s">"created"</span><span class="o">).</span><span class="na">asString</span><span class="o">();</span>

<span class="c1">// 26.7.2 and later: it is a real temporal value</span>
<span class="nc">ZonedDateTime</span> <span class="n">when</span> <span class="o">=</span> <span class="kd">record</span><span class="err">.</span><span class="nc">get</span><span class="o">(</span><span class="s">"created"</span><span class="o">).</span><span class="na">asZonedDateTime</span><span class="o">();</span>
</code></pre></div></div>

<p>This is what every Neo4j driver expects, and it is still a change, so the <a href="/blog/arcadedb-26-7-2/">26.7.2 release notes</a> call it out explicitly.</p>

<h2 id="the-part-that-keeps-it-true">The part that keeps it true</h2>

<p>A test suite that runs when someone remembers to run it eventually stops being true. The pipeline matters more than any single bug on that list.</p>

<p>All five language suites gate every pull request. On top of that, a nightly workflow runs the full cross-product: every scenario, every driver, every pinned version. Each language’s JUnit-style report becomes a set of per-cell results, the cells merge into a single <code class="language-plaintext highlighter-rouge">bolt-compat-matrix.json</code>, and that JSON is cross-referenced against the expected cell set from <code class="language-plaintext highlighter-rouge">driver-versions.md</code>. A cell that should exist and does not is treated exactly like a cell that ran and failed. Silence does not pass.</p>

<p>Three things then happen without anyone touching them:</p>

<ol>
  <li><strong>A regression issue opens itself.</strong> Any red cell auto-opens a <code class="language-plaintext highlighter-rouge">bolt-compat-regression</code> issue, which closes itself when the matrix goes green again.</li>
  <li><strong>The matrix is rendered and committed.</strong> A small renderer turns the nightly JSON plus the spec metadata into <a href="https://github.com/ArcadeData/arcadedb/blob/main/bolt/conformance/COMPATIBILITY.md"><code class="language-plaintext highlighter-rouge">COMPATIBILITY.md</code></a>. Rows are scenarios grouped by area, columns are <code class="language-plaintext highlighter-rouge">language:version</code>, and cells are pass, fail, known limitation, not applicable, or not reported. Non-passing cells link to their tracking issue. The page carries a “last verified” timestamp and a link to the CI run that produced it.</li>
  <li><strong>The badge updates.</strong> A shields.io endpoint in the README goes green only when every applicable cell passes.</li>
</ol>

<p>The red nights get published the same way the green ones do. A matrix that could only ever come out green would not be worth publishing.</p>

<h2 id="what-is-not-green">What is not green</h2>

<p>Every applicable cell passes today. Two cells are not green, both deliberately:</p>

<ul>
  <li><strong>ERR-003</strong> (unauthenticated request returns <code class="language-plaintext highlighter-rouge">Neo.ClientError.Security.Forbidden</code>) is marked <strong>not applicable</strong>. It cannot be triggered through any official driver’s public API, because every official driver completes HELLO/LOGON internally before it hands your code a session. Reaching it would take a bespoke raw-socket client, which our own rules exclude. The row stays in the matrix with the explanation attached.</li>
  <li><strong>CONN-004</strong> (<code class="language-plaintext highlighter-rouge">neo4j://</code> routing reflecting a real multi-node topology) is <strong>skipped</strong>, because the nightly runs against a single node. The HA-aware ROUTE implementation shipped in 26.7.2; the multi-node nightly harness for it did not. The cell carries a footnote about the <code class="language-plaintext highlighter-rouge">HA_SERVER_LIST</code> configuration it depends on, and it stays visible until something actually covers it.</li>
</ul>

<p>The Java 4.4.20 column is sparse on purpose. It runs the legacy-driver subset that proves 4.4 wire negotiation still works, not the full modern-API suite.</p>

<h2 id="try-it">Try it</h2>

<p>Point any official Neo4j driver at an ArcadeDB server. Nothing special is required, and the rest of what a move off Neo4j involves is on the <a href="/neo4j.html">Neo4j migration page</a>. Here is the same query in all five certified drivers:</p>

<div class="code-tabs">
  <div class="code-tabs-nav" role="tablist" aria-label="Driver language">
    <button class="code-tab active" role="tab" data-lang="java" aria-selected="true" tabindex="0">Java</button>
    <button class="code-tab" role="tab" data-lang="javascript" aria-selected="false" tabindex="-1">JavaScript</button>
    <button class="code-tab" role="tab" data-lang="python" aria-selected="false" tabindex="-1">Python</button>
    <button class="code-tab" role="tab" data-lang="csharp" aria-selected="false" tabindex="-1">C#</button>
    <button class="code-tab" role="tab" data-lang="go" aria-selected="false" tabindex="-1">Go</button>
  </div>

<div class="code-tab-pane active" role="tabpanel" data-lang="java">

    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="o">(</span><span class="nc">Driver</span> <span class="n">driver</span> <span class="o">=</span> <span class="nc">GraphDatabase</span><span class="o">.</span><span class="na">driver</span><span class="o">(</span><span class="s">"bolt://localhost:7687"</span><span class="o">,</span>
         <span class="nc">AuthTokens</span><span class="o">.</span><span class="na">basic</span><span class="o">(</span><span class="s">"root"</span><span class="o">,</span> <span class="s">"playwithdata"</span><span class="o">));</span>
     <span class="nc">Session</span> <span class="n">session</span> <span class="o">=</span> <span class="n">driver</span><span class="o">.</span><span class="na">session</span><span class="o">(</span><span class="nc">SessionConfig</span><span class="o">.</span><span class="na">forDatabase</span><span class="o">(</span><span class="s">"beer"</span><span class="o">)))</span> <span class="o">{</span>

  <span class="n">session</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="s">"MATCH (b:Beer)-[:HasCategory]-&gt;(c:Category) "</span>
            <span class="o">+</span> <span class="s">"WHERE c.name = $cat RETURN b.name AS name LIMIT 5"</span><span class="o">,</span>
      <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"cat"</span><span class="o">,</span> <span class="s">"Irish Ale"</span><span class="o">))</span>
      <span class="o">.</span><span class="na">forEachRemaining</span><span class="o">(</span><span class="n">r</span> <span class="o">-&gt;</span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">r</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"name"</span><span class="o">).</span><span class="na">asString</span><span class="o">()));</span>
<span class="o">}</span>
</code></pre></div>    </div>

  </div>

<div class="code-tab-pane" role="tabpanel" data-lang="javascript">

    <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">neo4j</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">neo4j-driver</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">driver</span> <span class="o">=</span> <span class="nx">neo4j</span><span class="p">.</span><span class="nf">driver</span><span class="p">(</span><span class="dl">'</span><span class="s1">bolt://localhost:7687</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">neo4j</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nf">basic</span><span class="p">(</span><span class="dl">'</span><span class="s1">root</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">playwithdata</span><span class="dl">'</span><span class="p">));</span>
<span class="kd">const</span> <span class="nx">session</span> <span class="o">=</span> <span class="nx">driver</span><span class="p">.</span><span class="nf">session</span><span class="p">({</span> <span class="na">database</span><span class="p">:</span> <span class="dl">'</span><span class="s1">beer</span><span class="dl">'</span> <span class="p">});</span>

<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">session</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">MATCH (b:Beer)-[:HasCategory]-&gt;(c:Category) </span><span class="dl">'</span> <span class="o">+</span>
  <span class="dl">'</span><span class="s1">WHERE c.name = $cat RETURN b.name AS name LIMIT 5</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">{</span> <span class="na">cat</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Irish Ale</span><span class="dl">'</span> <span class="p">}</span>
<span class="p">);</span>
<span class="nx">result</span><span class="p">.</span><span class="nx">records</span><span class="p">.</span><span class="nf">forEach</span><span class="p">(</span><span class="nx">r</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">)));</span>

<span class="k">await</span> <span class="nx">session</span><span class="p">.</span><span class="nf">close</span><span class="p">();</span>
<span class="k">await</span> <span class="nx">driver</span><span class="p">.</span><span class="nf">close</span><span class="p">();</span>
</code></pre></div>    </div>

  </div>

<div class="code-tab-pane" role="tabpanel" data-lang="python">

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">neo4j</span> <span class="kn">import</span> <span class="n">GraphDatabase</span>

<span class="n">driver</span> <span class="o">=</span> <span class="n">GraphDatabase</span><span class="p">.</span><span class="nf">driver</span><span class="p">(</span><span class="sh">"</span><span class="s">bolt://localhost:7687</span><span class="sh">"</span><span class="p">,</span>
                              <span class="n">auth</span><span class="o">=</span><span class="p">(</span><span class="sh">"</span><span class="s">root</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">playwithdata</span><span class="sh">"</span><span class="p">))</span>

<span class="k">with</span> <span class="n">driver</span><span class="p">.</span><span class="nf">session</span><span class="p">(</span><span class="n">database</span><span class="o">=</span><span class="sh">"</span><span class="s">beer</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">session</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="sh">"</span><span class="s">MATCH (b:Beer)-[:HasCategory]-&gt;(c:Category) </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">WHERE c.name = $cat RETURN b.name AS name LIMIT 5</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">cat</span><span class="o">=</span><span class="sh">"</span><span class="s">Irish Ale</span><span class="sh">"</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">result</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="n">record</span><span class="p">[</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">])</span>
</code></pre></div>    </div>

  </div>

<div class="code-tab-pane" role="tabpanel" data-lang="csharp">

    <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">driver</span> <span class="p">=</span> <span class="n">GraphDatabase</span><span class="p">.</span><span class="nf">Driver</span><span class="p">(</span><span class="s">"bolt://localhost:7687"</span><span class="p">,</span>
    <span class="n">AuthTokens</span><span class="p">.</span><span class="nf">Basic</span><span class="p">(</span><span class="s">"root"</span><span class="p">,</span> <span class="s">"playwithdata"</span><span class="p">));</span>
<span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">session</span> <span class="p">=</span> <span class="n">driver</span><span class="p">.</span><span class="nf">AsyncSession</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span> <span class="n">o</span><span class="p">.</span><span class="nf">WithDatabase</span><span class="p">(</span><span class="s">"beer"</span><span class="p">));</span>

<span class="kt">var</span> <span class="n">names</span> <span class="p">=</span> <span class="k">await</span> <span class="n">session</span><span class="p">.</span><span class="nf">ExecuteReadAsync</span><span class="p">(</span><span class="k">async</span> <span class="n">tx</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="kt">var</span> <span class="n">cursor</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tx</span><span class="p">.</span><span class="nf">RunAsync</span><span class="p">(</span>
        <span class="s">"MATCH (b:Beer)-[:HasCategory]-&gt;(c:Category) "</span> <span class="p">+</span>
        <span class="s">"WHERE c.name = $cat RETURN b.name AS name LIMIT 5"</span><span class="p">,</span>
        <span class="k">new</span> <span class="p">{</span> <span class="n">cat</span> <span class="p">=</span> <span class="s">"Irish Ale"</span> <span class="p">});</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">cursor</span><span class="p">.</span><span class="nf">ToListAsync</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="n">r</span><span class="p">[</span><span class="s">"name"</span><span class="p">].</span><span class="n">As</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;());</span>
<span class="p">});</span>

<span class="n">names</span><span class="p">.</span><span class="nf">ForEach</span><span class="p">(</span><span class="n">Console</span><span class="p">.</span><span class="n">WriteLine</span><span class="p">);</span>
</code></pre></div>    </div>

  </div>

<div class="code-tab-pane" role="tabpanel" data-lang="go">

    <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ctx</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">()</span>

<span class="n">driver</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">neo4j</span><span class="o">.</span><span class="n">NewDriverWithContext</span><span class="p">(</span><span class="s">"bolt://localhost:7687"</span><span class="p">,</span>
    <span class="n">neo4j</span><span class="o">.</span><span class="n">BasicAuth</span><span class="p">(</span><span class="s">"root"</span><span class="p">,</span> <span class="s">"playwithdata"</span><span class="p">,</span> <span class="s">""</span><span class="p">))</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
    <span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">defer</span> <span class="n">driver</span><span class="o">.</span><span class="n">Close</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>

<span class="n">session</span> <span class="o">:=</span> <span class="n">driver</span><span class="o">.</span><span class="n">NewSession</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">neo4j</span><span class="o">.</span><span class="n">SessionConfig</span><span class="p">{</span><span class="n">DatabaseName</span><span class="o">:</span> <span class="s">"beer"</span><span class="p">})</span>
<span class="k">defer</span> <span class="n">session</span><span class="o">.</span><span class="n">Close</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span>

<span class="n">result</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">session</span><span class="o">.</span><span class="n">Run</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span>
    <span class="s">"MATCH (b:Beer)-[:HasCategory]-&gt;(c:Category) "</span><span class="o">+</span>
        <span class="s">"WHERE c.name = $cat RETURN b.name AS name LIMIT 5"</span><span class="p">,</span>
    <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="n">any</span><span class="p">{</span><span class="s">"cat"</span><span class="o">:</span> <span class="s">"Irish Ale"</span><span class="p">})</span>
<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
    <span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">for</span> <span class="n">result</span><span class="o">.</span><span class="n">Next</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">Record</span><span class="p">()</span><span class="o">.</span><span class="n">Values</span><span class="p">[</span><span class="m">0</span><span class="p">])</span>
<span class="p">}</span>
</code></pre></div>    </div>

  </div>
</div>

<p>Managed transactions, bookmarks, <code class="language-plaintext highlighter-rouge">neo4j://</code> routing, and typed temporal and spatial values behave the way the driver documentation says they should. Where they do not, the matrix says so.</p>

<h2 id="why-we-bothered">Why we bothered</h2>

<p>A large part of the graph world evaluates a database by pointing an existing driver at it, running existing Cypher, and seeing what breaks. That deserves a real answer instead of a compatibility adjective.</p>

<p>It is the same instinct behind <a href="/blog/arcadedb-jepsen-tests-34-pass/">our Jepsen work</a>: publish the harness, publish the results, publish the parts that do not pass. All of it is open:</p>

<ul>
  <li><a href="https://github.com/ArcadeData/arcadedb/issues/4882">Epic #4882: Bolt Driver Compatibility Certification</a></li>
  <li><a href="https://github.com/ArcadeData/arcadedb/blob/main/bolt/conformance/COMPATIBILITY.md">The live compatibility matrix</a></li>
  <li><a href="https://github.com/ArcadeData/arcadedb/blob/main/bolt/conformance/spec.yaml">The conformance spec</a> and <a href="https://github.com/ArcadeData/arcadedb/blob/main/bolt/conformance/README.md">how it is consumed</a></li>
  <li><a href="https://github.com/ArcadeData/arcadedb/blob/main/bolt/SERVER_IDENTITY.md">The advertised server identity and why it says <code class="language-plaintext highlighter-rouge">Neo4j/5.26.0 compatible</code></a></li>
</ul>

<hr />

<p><strong>Get ArcadeDB 26.7.2</strong>: <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.7.2">GitHub Releases</a> or <code class="language-plaintext highlighter-rouge">docker pull arcadedata/arcadedb:26.7.2</code></p>

<p>Run a Bolt workload against ArcadeDB and hit a case the matrix does not cover? <a href="https://github.com/ArcadeData/arcadedb/issues">Open an issue</a>. A scenario we never thought to write is the most useful bug report we can get.</p>

<p>Roberto Franchini
Director R&amp;D</p>]]></content><author><name>Roberto Franchini</name></author><category term="Bolt" /><category term="Neo4j" /><category term="Graph Database" /><category term="Cypher" /><category term="Drivers" /><category term="Testing" /><category term="CI" /><category term="Compatibility" /><category term="ArcadeDB" /><summary type="html"><![CDATA[ArcadeDB 26.7.2 ships full Bolt driver certification: 39 conformance scenarios run nightly against the Java, JavaScript, Python, .NET, and Go Neo4j drivers across 14 pinned versions, published as a live compatibility matrix and a status badge.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/bolt-protocol-fidelity.png" /><media:content medium="image" url="https://arcadedb.com/assets/images/bolt-protocol-fidelity.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB 26.7.2: Deep Engine Audit, Neo4j Bolt Certification &amp;amp; 5 Critical Security Fixes</title><link href="https://arcadedb.com/blog/arcadedb-26-7-2/" rel="alternate" type="text/html" title="ArcadeDB 26.7.2: Deep Engine Audit, Neo4j Bolt Certification &amp;amp; 5 Critical Security Fixes" /><published>2026-07-09T00:00:00+00:00</published><updated>2026-07-09T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-26-7-2</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-26-7-2/"><![CDATA[<p>We’re pleased to announce <strong>ArcadeDB 26.7.2</strong>, a stability and correctness release that resolves <strong>over 130 issues</strong>. Where <a href="https://arcadedb.com/blog/arcadedb-26-7-1/">26.7.1</a> focused on Raft resilience, OpenTelemetry tracing, and standards alignment, 26.7.2 turns the spotlight on the <strong>deep internals</strong>: a full engine audit across storage, WAL recovery, transactions, LSM indexes, and concurrency. On top of that it completes <strong>Neo4j Bolt driver certification</strong>, hardens High Availability further, and ships <strong>five critical security fixes</strong>.</p>

<h2 id="major-highlights">Major Highlights</h2>

<h3 id="deep-engine-audit">Deep Engine Audit</h3>

<p>The heart of this release is a systematic audit of the storage and execution engine. These are the guarantees you rely on every commit, tightened across the board:</p>

<ul>
  <li><strong>Storage &amp; recovery:</strong> WAL append is now the transaction’s point of no return, with pre-write validation. Torn 64KB page writes are repaired during recovery, and the page cache prevents both lost updates and unbounded growth.</li>
  <li><strong>LSM index stability:</strong> Compaction no longer loses re-inserted keys or leaks orphaned pages, range scans are no longer truncated by tombstoned entries, and non-unique lookups no longer resurrect deleted records.</li>
  <li><strong>Transaction safety:</strong> Double-indexed updates within a single transaction no longer corrupt indexes, dead-thread cleanup rolls back abandoned transactions properly, and virtual-thread compatibility is restored.</li>
  <li><strong>Parallel query safety:</strong> A dedicated thread pool for bucket scans prevents deadlocks, each worker gets an isolated command context, and native iterators bound producer offers and close cleanly.</li>
</ul>

<p>These are the fixes you feel under load and during the unhappy path, not in a benchmark.</p>

<h3 id="neo4j-bolt-driver-certification">Neo4j Bolt Driver Certification</h3>

<p>ArcadeDB now achieves <strong>full compatibility certification</strong> with the Neo4j Bolt driver:</p>

<ul>
  <li>Native temporal PackStream structures: <code class="language-plaintext highlighter-rouge">date</code>, <code class="language-plaintext highlighter-rouge">time</code>, <code class="language-plaintext highlighter-rouge">datetime</code>, and <code class="language-plaintext highlighter-rouge">localdatetime</code>.</li>
  <li>Native <strong>Path</strong>, <strong>Duration</strong>, and <strong>Point</strong> types.</li>
  <li><strong>Retryable conflict mapping</strong> so the driver’s auto-retry works out of the box.</li>
  <li><strong>HA-aware ROUTE</strong> responses and <strong>Bolt 5.x</strong> negotiation support.</li>
  <li>Populated write-result counters.</li>
</ul>

<h3 id="security-hardening-5-critical-fixes">Security Hardening: 5 Critical Fixes</h3>

<p>This release patches five critical vulnerabilities. Upgrading is strongly recommended for internet-facing and multi-tenant deployments:</p>

<ul>
  <li><strong>RCE via JavaScript triggers:</strong> <code class="language-plaintext highlighter-rouge">java.lang.*</code> classes are removed from trigger allow-lists, restricting to benign packages only.</li>
  <li><strong>Arbitrary JavaScript execution:</strong> defining <code class="language-plaintext highlighter-rouge">js</code> functions now requires <code class="language-plaintext highlighter-rouge">UPDATE_SECURITY</code> with restricted polyglot access.</li>
  <li><strong>Secret disclosure via API:</strong> server settings now redact all hidden configuration keys, including cluster tokens.</li>
  <li><strong>Cross-database IDOR:</strong> authorization checks added to 14 HTTP handlers to prevent unauthorized database access.</li>
  <li><strong>Read-only mutation of schema:</strong> missing <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code> guards added to schema and config mutators.</li>
</ul>

<h3 id="high-availability-raft-enhancements">High Availability (Raft) Enhancements</h3>

<ul>
  <li><strong>Durable Raft storage now defaults to <code class="language-plaintext highlighter-rouge">true</code></strong>, preventing follower divergence.</li>
  <li>The leader <strong>auto-recovers wedged replication channels</strong>.</li>
  <li>A new <code class="language-plaintext highlighter-rouge">TransactionCommittedRemotelyException</code> (HTTP 409) prevents duplicate retries.</li>
  <li>Offline bootstrap leadership-transfer now commits a baseline.</li>
</ul>

<h2 id="major-fixes">Major Fixes</h2>

<h3 id="query-engine-opencypher">Query Engine: OpenCypher</h3>

<ul>
  <li>Dynamic property mutations (<code class="language-plaintext highlighter-rouge">SET n[key] = value</code>) are now applied.</li>
  <li>Inline relationship filters are enforced in <code class="language-plaintext highlighter-rouge">MATCH</code>, <code class="language-plaintext highlighter-rouge">exists()</code>, comprehensions, and shortest-path queries.</li>
  <li><code class="language-plaintext highlighter-rouge">FOREACH</code> updates are visible to subsequent <code class="language-plaintext highlighter-rouge">RETURN</code> clauses.</li>
  <li>Pattern comprehension and correlated subquery corrections.</li>
  <li>Temporal normalization is applied in index scans.</li>
</ul>

<h3 id="query-engine-sql">Query Engine: SQL</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">FROM</code> is now usable as a property name in DDL and queries.</li>
  <li><code class="language-plaintext highlighter-rouge">ORDER BY</code> on non-indexed properties returns correct results.</li>
  <li><code class="language-plaintext highlighter-rouge">TRAVERSE</code> with <code class="language-plaintext highlighter-rouge">MAXDEPTH</code> corrected; <code class="language-plaintext highlighter-rouge">BREADTH_FIRST</code> now performs true breadth-first traversal.</li>
  <li>Map indexing and array serialization fixes.</li>
</ul>

<h3 id="additional-features">Additional Features</h3>

<ul>
  <li>User-defined functions are now persisted and distributed across HA clusters.</li>
  <li>Configurable vector quantization for sparse vector indexes.</li>
  <li>Commutative append-merge removes retry storms on high-degree vertices.</li>
  <li>Extended Cypher write-counter surfacing over HTTP and gRPC.</li>
  <li>Point datatype and spatial index support.</li>
  <li>Python binding performance improvements.</li>
</ul>

<h3 id="dependencies">Dependencies</h3>

<p>Notable upgrades include Netty 4.2.16, Jackson 2.22.1, PostgreSQL JDBC 42.7.13, GraalVM 25.1.3, and Neo4j Java driver 6.2.0, plus the usual round of updates.</p>

<h2 id="breaking-changes">Breaking Changes</h2>

<p>Two behavioral changes to note when upgrading:</p>

<ol>
  <li><strong>Raft storage durability:</strong> <code class="language-plaintext highlighter-rouge">arcadedb.ha.raftPersistStorage</code> now defaults to <code class="language-plaintext highlighter-rouge">true</code>. Ensure the storage directory resides on durable media. Test clusters can opt out explicitly.</li>
  <li><strong>Bolt temporal types:</strong> temporal values are now transmitted as native PackStream structures instead of ISO-8601 strings. Clients must read native types (e.g., <code class="language-plaintext highlighter-rouge">asZonedDateTime()</code>) rather than strings.</li>
</ol>

<h2 id="getting-started-with-2672">Getting Started with 26.7.2</h2>

<h3 id="docker">Docker</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull arcadedata/arcadedb:26.7.2
</code></pre></div></div>

<p>Visit our <a href="https://hub.docker.com/r/arcadedata/arcadedb">Docker Hub repository</a> for more information.</p>

<h3 id="maven">Maven</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>com.arcadedb<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>arcadedb-engine<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;version&gt;</span>26.7.2<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>All artifacts are available on <a href="https://repo.maven.apache.org/maven2/com/arcadedb/">Maven Central</a>.</p>

<h3 id="documentation">Documentation</h3>

<p>For detailed information on features and usage, refer to our <a href="https://docs.arcadedb.com/">comprehensive documentation</a>.</p>

<h2 id="compatibility-note">Compatibility Note</h2>

<p>This release maintains 100% compatibility with previous database formats, meaning no export/import is required when upgrading. As always, we recommend creating a database backup before upgrading.</p>

<hr />

<p><strong>Download ArcadeDB 26.7.2 now</strong>: <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.7.2">GitHub Releases</a></p>

<p>Thanks to everyone in the community who reported issues, opened PRs, and helped shape this release.</p>

<p>Luca Garulli
ArcadeDB Founder</p>]]></content><author><name>Luca Garulli</name></author><category term="Multi-Model" /><category term="High Availability" /><category term="Security" /><category term="Neo4j" /><category term="Graph Database" /><category term="Release" /><summary type="html"><![CDATA[ArcadeDB 26.7.2 is a stability and correctness release with 130+ resolved issues: a deep engine audit across storage, WAL recovery, transactions, LSM indexes and concurrency, full Neo4j Bolt driver certification, Raft HA hardening, and 5 critical security fixes.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/release-v26.7.2.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/release-v26.7.2.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB Cloud Observability: OpenTelemetry Tracing, Structured Logging, and Kubernetes Health Probes</title><link href="https://arcadedb.com/blog/arcadedb-cloud-observability-opentelemetry-kubernetes/" rel="alternate" type="text/html" title="ArcadeDB Cloud Observability: OpenTelemetry Tracing, Structured Logging, and Kubernetes Health Probes" /><published>2026-07-07T00:00:00+00:00</published><updated>2026-07-07T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-cloud-observability-opentelemetry-kubernetes</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-cloud-observability-opentelemetry-kubernetes/"><![CDATA[<p>A database in Kubernetes has to answer two questions the orchestrator keeps asking: are you alive, and are you ready? It also has to answer the one an on-call engineer asks at 2am, which is why a query that normally takes 8ms just took four seconds. ArcadeDB could answer none of them well. The <a href="https://github.com/ArcadeData/arcadedb/issues/4463">Cloud Observability Architecture</a> work fixes that with four pieces that ship independently: health probes, deeper metrics with OTLP export, structured logging with correlation IDs, and distributed tracing over OpenTelemetry.</p>

<p>All of it is opt-in. Touch no configuration key and your server behaves exactly as it does today. For how the server is deployed in the first place, see the <a href="/client-server.html">client-server page</a>.</p>

<h2 id="what-was-already-there-and-what-wasnt">What was already there, and what wasn’t</h2>

<p>Single-node observability was in decent shape: <a href="https://micrometer.io/">Micrometer</a> metrics with an in-memory registry, an optional <code class="language-plaintext highlighter-rouge">/prometheus</code> endpoint, JVM and executor-pool binders, an engine <code class="language-plaintext highlighter-rouge">Profiler</code> that tracks cache hit ratio, pages, WAL, transaction counts, and MVCC conflicts, a manual query profiler, and an <code class="language-plaintext highlighter-rouge">/api/v1/ready</code> endpoint.</p>

<p>The gaps only show up once you put that server in a cluster. Query, command, and transaction latency lived in the manual profiler, which means you had to go looking for it; there were no always-on RED (Rate, Errors, Duration) timers and no percentile histograms. There were no spans at all, so a request that crossed the query engine, a transaction, and a Raft replication hop was three disconnected mysteries. Logs were text-only, with nothing tying a line back to the request that produced it. And there was a readiness endpoint but no liveness check, which is the one Kubernetes reaches for first.</p>

<h2 id="instrument-once-get-two-signals">Instrument once, get two signals</h2>

<p>The part of this design I like most is the one that required the least new code. Micrometer’s Observation API lets you instrument a code path once and emit either a timer or a span from it, depending on what happens to be registered at runtime. ArcadeDB already shipped Micrometer, so the core server now wraps its hot paths (HTTP request handling, query and command execution, transaction commit, and Raft replication) in <code class="language-plaintext highlighter-rouge">Observation</code> calls and stops there.</p>

<p>With no tracer registered, an <code class="language-plaintext highlighter-rouge">Observation</code> is a metrics-only timer: what already happened, under a new name. Install the tracing plugin and those same call sites start producing spans as well. No second instrumentation pass, no parallel set of trace annotations drifting out of sync with the metric ones. If you have ever maintained a codebase where the metrics and the traces disagree about what a “query” is, you know why this matters.</p>

<h2 id="metrics-red-timers-and-otlp-export">Metrics: RED timers and OTLP export</h2>

<p>Three always-on timers, with percentile histograms and SLO buckets, exposed through <code class="language-plaintext highlighter-rouge">/prometheus</code> and optionally through OTLP:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">arcadedb.http.requests</code>, tagged by method, path template, status, and database. The path template is deliberate: tagging by raw URI is how you blow up your cardinality budget and get a call from whoever pays the metrics bill.</li>
  <li><code class="language-plaintext highlighter-rouge">arcadedb.query.duration</code>, tagged by language, database, and query type.</li>
  <li><code class="language-plaintext highlighter-rouge">arcadedb.tx.duration</code>, tagged by database and outcome (commit or rollback).</li>
</ul>

<p>A new <code class="language-plaintext highlighter-rouge">EngineMetricsBinder</code>, modeled on the existing <code class="language-plaintext highlighter-rouge">PoolMetrics</code>, pulls the engine <code class="language-plaintext highlighter-rouge">Profiler</code>’s cache hit ratio, page and WAL statistics, MVCC conflict counts, and the database-level and sparse-vector numbers into Micrometer gauges tagged by database. Set <code class="language-plaintext highlighter-rouge">arcadedb.serverMetrics.otlp.enabled=true</code> and the same series push to an OTLP collector. The Prometheus scrape path is not touched.</p>

<h2 id="tracing-in-a-module-you-can-leave-on-the-shelf">Tracing, in a module you can leave on the shelf</h2>

<p>Tracing lives in a separate, optional <code class="language-plaintext highlighter-rouge">tracing</code> module, packaged the same way the <code class="language-plaintext highlighter-rouge">metrics</code> module already is: its own Maven module, <code class="language-plaintext highlighter-rouge">provided</code> scope on the server, loaded through the <code class="language-plaintext highlighter-rouge">ServerPlugin</code> SPI. The OpenTelemetry SDK, <code class="language-plaintext highlighter-rouge">micrometer-tracing-bridge-otel</code>, and the OTLP exporter stay inside it. The core server’s compile classpath never sees them, which was a hard requirement: nobody should inherit the OTel dependency tree because they wanted a graph database.</p>

<p>Set <code class="language-plaintext highlighter-rouge">arcadedb.serverMetrics.tracing.enabled=true</code> and the plugin registers a bridged tracer into Micrometer’s global <code class="language-plaintext highlighter-rouge">ObservationRegistry</code>. The Observations described above start producing spans, nested from HTTP down through query execution and transaction commit. Inbound requests continue an upstream trace through the W3C <code class="language-plaintext highlighter-rouge">traceparent</code> header, and outbound Raft RPCs propagate context, so a write is traceable from leader to follower.</p>

<p>Leave the jar off the classpath or the flag unset and it is a genuine no-op. No span overhead, no registry, nothing.</p>

<h2 id="structured-logging-and-correlation-ids">Structured logging and correlation IDs</h2>

<p>A timer tells you the p99 moved. A span tells you which hop ate the time. Neither tells you about the <code class="language-plaintext highlighter-rouge">IOException</code> that got swallowed and retried, and that is usually the thing you actually needed. So the logs have to join the same conversation.</p>

<p>At the start of each request, ArcadeDB populates a diagnostic context with the active trace and span IDs, plus a generated request ID so correlation still works when tracing is off. The context is scoped per request and cleared in a <code class="language-plaintext highlighter-rouge">finally</code> block, because the server hands threads back to a worker pool and a leaked MDC entry means the next request inherits someone else’s trace ID. That bug is miserable to find. The <code class="language-plaintext highlighter-rouge">finally</code> is not decorative.</p>

<p>An opt-in <code class="language-plaintext highlighter-rouge">JsonLogFormatter</code> (<code class="language-plaintext highlighter-rouge">arcadedb.server.logFormat=json</code>) writes one JSON object per line: timestamp, level, logger, thread, message, trace ID, span ID, database, request ID, exception. It is built on ArcadeDB’s existing <code class="language-plaintext highlighter-rouge">JSONObject</code>, so it adds no JSON dependency. If you want correlation without moving to JSON, <code class="language-plaintext highlighter-rouge">arcadedb.server.logIncludeTrace=true</code> appends a <code class="language-plaintext highlighter-rouge">[traceId=...]</code> tag to the text format instead.</p>

<h2 id="health-probes-for-kubernetes">Health probes for Kubernetes</h2>

<p>This is the smallest change in the whole set and probably the one most people will use: <code class="language-plaintext highlighter-rouge">GET /api/v1/health</code>. It is deliberately cheap. No database I/O, no auth, returns <code class="language-plaintext highlighter-rouge">200</code> as long as the process and the HTTP layer are up.</p>

<p>Cheapness is the entire point. Liveness must not depend on database readiness. Wire a readiness-style check into <code class="language-plaintext highlighter-rouge">livenessProbe</code> and Kubernetes will helpfully kill a node that is still replaying its WAL, then kill the replacement for the same reason, and you will spend an afternoon reading kubelet events before you work out that your health check is the outage.</p>

<p><code class="language-plaintext highlighter-rouge">/api/v1/ready</code> is unchanged by default. It gains one optional behavior: with <code class="language-plaintext highlighter-rouge">arcadedb.server.readinessRequiresHA=true</code> and HA active, readiness reports false until the node has joined the Raft group and caught up. That gives clustered deployments a <code class="language-plaintext highlighter-rouge">readinessProbe</code> that means something.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livenessProbe</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/api/v1/health</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">2480</span>
  <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">10</span>
<span class="na">readinessProbe</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/api/v1/ready</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">2480</span>
  <span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
  <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>

<h2 id="configuration-reference">Configuration reference</h2>

<p>Every new key defaults to off, or to what the server does today:</p>

<table>
  <thead>
    <tr>
      <th>Key</th>
      <th>Default</th>
      <th>Effect</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.serverMetrics.otlp.enabled</code></td>
      <td><code class="language-plaintext highlighter-rouge">false</code></td>
      <td>Register an OTLP metrics registry alongside Prometheus</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.serverMetrics.otlp.endpoint</code></td>
      <td><code class="language-plaintext highlighter-rouge">http://localhost:4317</code></td>
      <td>OTLP metrics endpoint</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.serverMetrics.tracing.enabled</code></td>
      <td><code class="language-plaintext highlighter-rouge">false</code></td>
      <td>Activate the tracing plugin (no-op if the jar isn’t present)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.serverMetrics.tracing.endpoint</code></td>
      <td><code class="language-plaintext highlighter-rouge">http://localhost:4317</code></td>
      <td>OTLP trace endpoint</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.serverMetrics.tracing.samplingRate</code></td>
      <td><code class="language-plaintext highlighter-rouge">0.0</code></td>
      <td>Parent-based sampling ratio</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.server.logFormat</code></td>
      <td><code class="language-plaintext highlighter-rouge">text</code></td>
      <td><code class="language-plaintext highlighter-rouge">json</code> selects the structured <code class="language-plaintext highlighter-rouge">JsonLogFormatter</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.server.logIncludeTrace</code></td>
      <td><code class="language-plaintext highlighter-rouge">false</code></td>
      <td>Append <code class="language-plaintext highlighter-rouge">[traceId=...]</code> to text-mode logs</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arcadedb.server.readinessRequiresHA</code></td>
      <td><code class="language-plaintext highlighter-rouge">false</code></td>
      <td>Make <code class="language-plaintext highlighter-rouge">/api/v1/ready</code> HA-aware (Raft membership and catch-up)</td>
    </tr>
  </tbody>
</table>

<h2 id="on-not-breaking-anything">On not breaking anything</h2>

<p>The whole design was built against one constraint: with no new configuration, an upgraded deployment behaves exactly as it did before. Existing endpoints and response shapes (<code class="language-plaintext highlighter-rouge">/prometheus</code>, <code class="language-plaintext highlighter-rouge">/api/v1/server</code>, <code class="language-plaintext highlighter-rouge">/api/v1/ready</code>) were added to, never modified. No configuration key was renamed. No new mandatory dependency lands on the core server, which is why the OpenTelemetry SDK sits behind the SPI in its own module rather than in the server <code class="language-plaintext highlighter-rouge">pom.xml</code> where it would have been considerably easier to put it.</p>

<p>Upgrade whenever you like, then turn things on one flag at a time.</p>

<h2 id="pairing-it-with-grafana">Pairing it with Grafana</h2>

<p>If you already run the <a href="/blog/arcadedb-grafana-plugin-bi-dashboards-for-your-multi-model-database/">ArcadeDB Grafana plugin</a> for BI dashboards over SQL, Cypher, and Gremlin, the OTLP metrics and traces drop into the same stack. Point an OpenTelemetry Collector at the server, fan out to Prometheus and Tempo (or whatever your vendor of the month is), and you can go from a p99 spike on a dashboard to the trace behind it to the log line that explains it, without leaving the cluster.</p>]]></content><author><name>Roberto Franchini</name></author><category term="Observability" /><category term="OpenTelemetry" /><category term="Kubernetes" /><category term="Metrics" /><category term="Tracing" /><category term="Logging" /><category term="Prometheus" /><category term="Micrometer" /><category term="Grafana" /><category term="DevOps" /><summary type="html"><![CDATA[ArcadeDB adds OpenTelemetry tracing, RED latency metrics with OTLP export, structured JSON logging with correlation IDs, and Kubernetes health probes. Every one of them is opt-in and backward compatible.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/arcadedb-cloud-observability.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/arcadedb-cloud-observability.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB 26.7.1: Raft Hardening, OpenTelemetry Tracing, BM25 &amp;amp; GQL</title><link href="https://arcadedb.com/blog/arcadedb-26-7-1/" rel="alternate" type="text/html" title="ArcadeDB 26.7.1: Raft Hardening, OpenTelemetry Tracing, BM25 &amp;amp; GQL" /><published>2026-07-01T00:00:00+00:00</published><updated>2026-07-01T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-26-7-1</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-26-7-1/"><![CDATA[<p>We’re pleased to announce <strong>ArcadeDB 26.7.1</strong>, a large stability, resilience, and security release with <strong>420+ commits</strong> resolving <strong>238 issues</strong>. Where <a href="https://arcadedb.com/blog/arcadedb-26-6-1/">26.6.1</a> added encrypted HA clusters and a durability hardening pass, 26.7.1 goes deeper into <strong>cluster resilience</strong>: the Raft engine now degrades gracefully instead of halting, diverged followers heal themselves, and the whole system is far easier to observe in production thanks to <strong>OpenTelemetry distributed tracing</strong>. On top of that come native <strong>BM25</strong> full-text scoring, <strong>ISO GQL</strong> standards alignment, and another broad security sweep.</p>

<h2 id="major-highlights">Major Highlights</h2>

<h3 id="raft--high-availability-hardening">Raft &amp; High Availability Hardening</h3>

<p>The headline of this release is resilience. A cluster should survive a bad entry, a network blip, or a diverged replica without operator intervention:</p>

<ul>
  <li>A failed apply on one database <strong>no longer halts the entire node</strong>. The affected database is put into <strong>per-database quarantine</strong> while every other database on the node keeps serving traffic.</li>
  <li>Diverged followers can now <strong>self-recover</strong> back to the leader’s state instead of getting stuck.</li>
  <li>Snapshot integrity is strengthened with <strong>fsync on write</strong> and <strong>manifest verification</strong>, so a follower never installs a truncated or corrupt snapshot.</li>
  <li>Membership changes are applied as <strong>atomic deltas under quorum protection</strong>, closing the classic split-brain and lost-quorum windows.</li>
  <li><strong>Silent write loss</strong> on replication timeouts has been eliminated: a write that cannot be safely replicated is reported, not dropped.</li>
  <li><code class="language-plaintext highlighter-rouge">TRUNCATE TYPE</code> and <code class="language-plaintext highlighter-rouge">TRUNCATE BUCKET</code> are now <strong>HA-safe</strong>, follower index correlation is fixed by stable peer-id, and a stalled replica stuck at <code class="language-plaintext highlighter-rouge">matchIndex=-1</code> now auto-recovers.</li>
</ul>

<p>These are the fixes you feel in production during the unhappy path, not in a benchmark.</p>

<h3 id="opentelemetry-distributed-tracing--structured-logging">OpenTelemetry Distributed Tracing &amp; Structured Logging</h3>

<p>Observability is now first-class. A new <strong>OpenTelemetry</strong> module provides end-to-end distributed tracing with optional <strong>OTLP export</strong>, so you can follow a request across the wire protocols, the query engine, and the storage layer in your existing tracing backend. Alongside it:</p>

<ul>
  <li><strong>Structured JSON logging</strong> with per-request <strong>correlation IDs</strong>, so a single request is trivial to reconstruct across logs and traces.</li>
  <li>Enhanced metrics with <strong>RED timers</strong> (Rate, Errors, Duration) and engine gauges.</li>
</ul>

<h3 id="native-bm25-full-text-scoring">Native BM25 Full-Text Scoring</h3>

<p>Full-text search now ships a native <strong>BM25</strong> scoring implementation with <strong>field boosts</strong> and <strong>caret (<code class="language-plaintext highlighter-rouge">^</code>) boost syntax</strong>, so relevance ranking is tunable per field. <code class="language-plaintext highlighter-rouge">EXPLAIN</code> and <code class="language-plaintext highlighter-rouge">PROFILE</code> now cover search operations too, making it easy to see how a full-text query is planned and executed.</p>

<h3 id="iso-gql-standards-alignment">ISO GQL Standards Alignment</h3>

<p>ArcadeDB continues moving toward the <strong>ISO GQL</strong> graph query standard:</p>

<ul>
  <li><strong>Session management</strong> statements: <code class="language-plaintext highlighter-rouge">SESSION SET</code> / <code class="language-plaintext highlighter-rouge">SESSION RESET</code> / <code class="language-plaintext highlighter-rouge">SESSION CLOSE</code>.</li>
  <li><strong>Transaction control</strong> statements: <code class="language-plaintext highlighter-rouge">START TRANSACTION</code> / <code class="language-plaintext highlighter-rouge">COMMIT</code> / <code class="language-plaintext highlighter-rouge">ROLLBACK</code>.</li>
  <li><strong>Strict numeric types</strong> for standards-compliant arithmetic and comparisons.</li>
</ul>

<h3 id="security-hardening">Security Hardening</h3>

<p>Another broad security sweep tightens the defaults for multi-tenant and internet-facing deployments:</p>

<ul>
  <li><strong>Polyglot scripting</strong> now requires <strong>administrative privileges</strong>.</li>
  <li><strong>Centralized gRPC authorization</strong> enforcement across all endpoints.</li>
  <li>Additional <strong>path-traversal</strong> protection hardening.</li>
  <li><strong>DoS bounding</strong> on gRPC result materialization.</li>
  <li><strong>Cluster-management endpoints</strong> are now restricted to root access.</li>
</ul>

<h2 id="major-fixes">Major Fixes</h2>

<h3 id="vector-engine">Vector Engine</h3>

<ul>
  <li>New import/interop formats: <strong>MATLAB</strong>, <strong>MATLAB_COLUMN</strong>, <strong>JULIA</strong>, and <strong>NUMPY</strong>.</li>
  <li>New helper functions: <strong><code class="language-plaintext highlighter-rouge">asVector()</code></strong>, <strong><code class="language-plaintext highlighter-rouge">asSparse()</code></strong>, and <strong><code class="language-plaintext highlighter-rouge">vectorDequantizeBinary</code></strong>.</li>
  <li>Improved <strong>RRF</strong> (Reciprocal Rank Fusion) array input handling.</li>
</ul>

<h3 id="mongodb-protocol">MongoDB Protocol</h3>

<ul>
  <li>Added <strong><code class="language-plaintext highlighter-rouge">update</code></strong>, <strong><code class="language-plaintext highlighter-rouge">delete</code></strong>, and <strong><code class="language-plaintext highlighter-rouge">createIndexes</code></strong> commands.</li>
  <li><strong>SASL PLAIN</strong> authentication support.</li>
  <li>Improved <code class="language-plaintext highlighter-rouge">find</code> command data handling.</li>
</ul>

<h3 id="kubernetes-operations">Kubernetes Operations</h3>

<ul>
  <li>Health probe endpoints for <strong>liveness</strong>, <strong>readiness</strong>, and <strong>startup</strong>.</li>
  <li><strong>Auto-acquire</strong> capability for databases the node has not seen yet.</li>
  <li><strong>StatefulSet scale-up</strong> support beyond a static peer list.</li>
</ul>

<h3 id="sql--queries">SQL &amp; Queries</h3>

<ul>
  <li>Map/collection <strong>key removal</strong> now persists correctly in <code class="language-plaintext highlighter-rouge">UPDATE</code>.</li>
  <li>Improved <strong>index selection</strong> for composite-key queries.</li>
  <li><strong>Three-valued logic</strong> for NULL operands in <code class="language-plaintext highlighter-rouge">IN</code> / <code class="language-plaintext highlighter-rouge">NOT IN</code>.</li>
  <li>Correct <strong>mixed-type numeric handling</strong> in <code class="language-plaintext highlighter-rouge">GROUP BY</code> and aggregation functions.</li>
</ul>

<h3 id="storage--recovery">Storage &amp; Recovery</h3>

<ul>
  <li><strong>Schema recovery from the backup file</strong> when the primary schema is corrupted.</li>
  <li>Corrupt <strong>hash-index detection and recovery</strong>.</li>
  <li>Time-series <strong>negative-timestamp alignment</strong> fixes.</li>
  <li>Page I/O locking improvements.</li>
</ul>

<h3 id="wire-protocols">Wire Protocols</h3>

<ul>
  <li><strong>gRPC:</strong> <code class="language-plaintext highlighter-rouge">InsertStream</code> now terminates correctly after a commit failure.</li>
  <li><strong>DATETIME precision</strong> is preserved across microsecond/nanosecond formats.</li>
  <li><strong>OpenCypher</strong> now tolerates dangling edges.</li>
  <li>Improved client-side <strong>nested type hydration</strong>.</li>
</ul>

<h3 id="dependencies">Dependencies</h3>

<p>Notable upgrades include Netty 4.2.15, Lucene 10.5.0, OpenTelemetry BOM 1.63.0, Jackson Databind 2.22.0, and JVector 4.0.0-rc.8, plus the usual round of Studio frontend, e2e harness, and CI updates.</p>

<h2 id="getting-started-with-2671">Getting Started with 26.7.1</h2>

<h3 id="docker">Docker</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull arcadedata/arcadedb:26.7.1
</code></pre></div></div>

<p>Visit our <a href="https://hub.docker.com/r/arcadedata/arcadedb">Docker Hub repository</a> for more information.</p>

<h3 id="maven">Maven</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>com.arcadedb<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>arcadedb-engine<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;version&gt;</span>26.7.1<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>All artifacts are available on <a href="https://repo.maven.apache.org/maven2/com/arcadedb/">Maven Central</a>.</p>

<h3 id="documentation">Documentation</h3>

<p>For detailed information on features and usage, refer to our <a href="https://docs.arcadedb.com/">comprehensive documentation</a>.</p>

<h2 id="compatibility-note">Compatibility Note</h2>

<p>This release maintains 100% compatibility with previous database formats, meaning no export/import is required when upgrading. As always, we recommend creating a database backup before upgrading.</p>

<hr />

<p><strong>Download ArcadeDB 26.7.1 now</strong>: <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.7.1">GitHub Releases</a></p>

<p>Thanks to everyone in the community who reported issues, opened PRs, and helped shape this release.</p>

<p>Luca Garulli
ArcadeDB Founder</p>]]></content><author><name>Luca Garulli</name></author><category term="Multi-Model" /><category term="High Availability" /><category term="Observability" /><category term="Security" /><category term="Graph Database" /><category term="Release" /><summary type="html"><![CDATA[ArcadeDB 26.7.1 is a large stability, resilience and security release with 420+ commits and 238 resolved issues: Raft/HA hardening with per-database quarantine and divergence self-recovery, OpenTelemetry distributed tracing, native BM25 full-text scoring, ISO GQL alignment, and a broad security sweep.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/release-v26.7.1.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/release-v26.7.1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>