<?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-10-30T22:15:31+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">Fraud Detection on a Graph: Finding the Ring That Every Row Looks Innocent In</title><link href="https://arcadedb.com/blog/fraud-detection-graph-rings/" rel="alternate" type="text/html" title="Fraud Detection on a Graph: Finding the Ring That Every Row Looks Innocent In" /><published>2026-10-20T00:00:00+00:00</published><updated>2026-10-20T00:00:00+00:00</updated><id>https://arcadedb.com/blog/fraud-detection-graph-rings</id><content type="html" xml:base="https://arcadedb.com/blog/fraud-detection-graph-rings/"><![CDATA[<p>A fraud analyst pulls up five accounts. Each one has a plausible name, a valid address, a working phone number, a funding source, and a transaction history that looks like everyone else’s. Individually there is nothing to act on. Decline any one of them and you have declined a customer.</p>

<p>The fraud is not in any of the five records. It is that account A’s phone number is account B’s, account B’s device fingerprint is account C’s, account C’s shipping address is account D’s, account D’s email is account E’s, and E is the beneficiary on a transfer from A. In a table that is five unremarkable rows. In a graph it is a closed loop you can see at a glance.</p>

<p>Fraud lives in the connections, which is why <a href="/fraud-detection.html">fraud detection</a> is one of the few workloads where a graph database is the better tool on the merits, not because graphs are in fashion.</p>

<h2 id="the-shape-of-the-problem">The shape of the problem</h2>

<p>Most of what a fraud team chases falls into three schemes, and each one is a pattern in the edges.</p>

<p><strong>Fraud rings.</strong> A set of accounts linked through shared attributes or circular money movement. The money leaves and comes back. No single account looks wrong.</p>

<p><strong>Synthetic identities.</strong> An identity assembled from real and fabricated details, built up over months of legitimate behavior before the bust-out. The giveaway is rarely a bad record. It is a cluster of records that are suspiciously similar to each other without being identical.</p>

<p><strong>Layering and smurfing.</strong> Money broken into small amounts and moved through intermediaries so that the path from origin to destination is obscured. Each hop is a legitimate-looking transfer, and the scheme only shows when you look at the whole path.</p>

<p>Every one of those is a question about relationships at a depth you do not know in advance, and the depth is what makes it hard. If you knew the ring was exactly three accounts, you could write the join. You do not, so you cannot.</p>

<h2 id="modeling-it">Modeling it</h2>

<p>Keep the model boring. Accounts, devices, and identifiers are vertices; transfers and usage are edges.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="k">TYPE</span> <span class="n">Account</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="k">TYPE</span> <span class="n">Device</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="k">TYPE</span> <span class="n">Identifier</span><span class="p">;</span>   <span class="c1">-- phone, email, address, tax id</span>

<span class="k">CREATE</span> <span class="n">EDGE</span> <span class="k">TYPE</span> <span class="n">TRANSFERRED</span><span class="p">;</span>    <span class="c1">-- Account -&gt; Account, with amount and timestamp</span>
<span class="k">CREATE</span> <span class="n">EDGE</span> <span class="k">TYPE</span> <span class="n">USED</span><span class="p">;</span>           <span class="c1">-- Account -&gt; Device</span>
<span class="k">CREATE</span> <span class="n">EDGE</span> <span class="k">TYPE</span> <span class="n">CLAIMS</span><span class="p">;</span>         <span class="c1">-- Account -&gt; Identifier</span>
</code></pre></div></div>

<p>The one modeling decision that matters: <strong>make shared attributes into vertices, not properties.</strong> If a phone number is a property on <code class="language-plaintext highlighter-rouge">Account</code>, then “find every account sharing a phone number” is a self-join over a string column. If it is an <code class="language-plaintext highlighter-rouge">Identifier</code> vertex that accounts point at, it is a two-hop traversal, and the shared identifier is a first-class thing you can attach a confidence score or a review status to.</p>

<h2 id="finding-the-ring">Finding the ring</h2>

<p>A ring is a path that returns to where it began. That is one query, and the depth is a parameter rather than a rewrite. Every query in this section comes in both Cypher and ArcadeDB SQL; use the tabs to switch, and use whichever your team already reads. Both run against the same data in the same engine.</p>

<div class="code-tabs">
  <div class="code-tabs-nav" role="tablist" aria-label="Ring query language">
    <button class="code-tab active" role="tab" data-lang="cypher" aria-selected="true" tabindex="0">Cypher</button>
    <button class="code-tab" role="tab" data-lang="sql" aria-selected="false" tabindex="-1">SQL</button>
  </div>

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

    <div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span> <span class="n">path</span> <span class="o">=</span> <span class="ss">(</span><span class="py">a:</span><span class="n">Account</span><span class="ss">)</span><span class="o">-</span><span class="ss">[</span><span class="nc">:TRANSFERRED</span><span class="o">*</span><span class="m">2</span><span class="o">..</span><span class="m">5</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">(</span><span class="n">a</span><span class="ss">)</span>
<span class="k">WHERE</span> <span class="n">a.id</span> <span class="o">=</span> <span class="n">$accountId</span>
<span class="k">RETURN</span> <span class="n">path</span>
</code></pre></div>    </div>

  </div>

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

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span> <span class="p">{</span><span class="k">type</span><span class="p">:</span> <span class="n">Account</span><span class="p">,</span> <span class="k">as</span><span class="p">:</span> <span class="n">a</span><span class="p">,</span> <span class="k">where</span><span class="p">:</span> <span class="p">(</span><span class="n">id</span> <span class="o">=</span> <span class="p">:</span><span class="n">accountId</span><span class="p">)}</span>
      <span class="p">.</span><span class="k">out</span><span class="p">(</span><span class="s1">'TRANSFERRED'</span><span class="p">){</span><span class="n">while</span><span class="p">:</span> <span class="p">(</span><span class="err">$</span><span class="n">depth</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">),</span>
                          <span class="k">where</span><span class="p">:</span> <span class="p">(</span><span class="err">$</span><span class="n">depth</span> <span class="o">&gt;=</span> <span class="mi">2</span> <span class="k">AND</span> <span class="o">@</span><span class="n">rid</span> <span class="o">=</span> <span class="err">$</span><span class="n">matched</span><span class="p">.</span><span class="n">a</span><span class="p">.</span><span class="o">@</span><span class="n">rid</span><span class="p">),</span>
                          <span class="n">pathAlias</span><span class="p">:</span> <span class="n">ring</span><span class="p">}</span>
<span class="k">RETURN</span> <span class="n">ring</span><span class="p">.</span><span class="n">id</span> <span class="k">AS</span> <span class="n">accounts</span>
</code></pre></div>    </div>

  </div>
</div>

<p><code class="language-plaintext highlighter-rouge">*2..5</code> in Cypher, and the <code class="language-plaintext highlighter-rouge">$depth</code> bounds in SQL, mean: follow <code class="language-plaintext highlighter-rouge">TRANSFERRED</code> edges between two and five hops, and only return paths that arrive back at the account they started from. Ask for six hops instead and you change one number. In a relational database each extra hop is another self-join, so a deeper ring means a different query.</p>

<p>The ring from the opening closes over a phone, a device, an address, an email and one transfer. In Cypher that is the same query with more edge types and no direction, since a shared identifier is one hop out and one hop back:</p>

<div class="code-tabs">
  <div class="code-tabs-nav" role="tablist" aria-label="Mixed ring query language">
    <button class="code-tab active" role="tab" data-lang="cypher" aria-selected="true" tabindex="0">Cypher</button>
    <button class="code-tab" role="tab" data-lang="sql" aria-selected="false" tabindex="-1">SQL</button>
  </div>

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

    <div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span> <span class="n">path</span> <span class="o">=</span> <span class="ss">(</span><span class="py">a:</span><span class="n">Account</span><span class="ss">)</span><span class="o">-</span><span class="ss">[</span><span class="nc">:TRANSFERRED</span><span class="o">|</span><span class="n">CLAIMS</span><span class="o">|</span><span class="n">USED</span><span class="o">*</span><span class="m">2</span><span class="o">..</span><span class="m">10</span><span class="ss">]</span><span class="o">-</span><span class="ss">(</span><span class="n">a</span><span class="ss">)</span>
<span class="k">WHERE</span> <span class="n">a.id</span> <span class="o">=</span> <span class="n">$accountId</span>
<span class="k">RETURN</span> <span class="ss">[</span><span class="n">n</span> <span class="ow">IN</span> <span class="nf">nodes</span><span class="ss">(</span><span class="n">path</span><span class="ss">)</span> <span class="k">WHERE</span> <span class="py">n:</span><span class="n">Account</span> <span class="o">|</span> <span class="n">n.id</span><span class="ss">]</span> <span class="k">AS</span> <span class="n">accounts</span>
</code></pre></div>    </div>

  </div>

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

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">@</span><span class="k">in</span><span class="p">.</span><span class="n">id</span> <span class="k">AS</span> <span class="n">beneficiary</span><span class="p">,</span>
       <span class="n">shortestPath</span><span class="p">(</span><span class="o">@</span><span class="k">in</span><span class="p">,</span> <span class="o">@</span><span class="k">out</span><span class="p">,</span> <span class="s1">'BOTH'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'CLAIMS'</span><span class="p">,</span> <span class="s1">'USED'</span><span class="p">],</span> <span class="p">{</span><span class="n">maxDepth</span><span class="p">:</span> <span class="mi">10</span><span class="p">})</span>
         <span class="p">[</span><span class="o">@</span><span class="k">type</span> <span class="o">=</span> <span class="s1">'Account'</span><span class="p">].</span><span class="n">id</span> <span class="k">AS</span> <span class="n">ring</span>
<span class="k">FROM</span> <span class="n">TRANSFERRED</span>
<span class="k">WHERE</span> <span class="o">@</span><span class="k">out</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="p">:</span><span class="n">accountId</span>
</code></pre></div>    </div>

  </div>
</div>

<p>Each shared identifier costs two hops, so the five-account ring above is nine edges long, and the bound is set with that in mind. The SQL version comes at it from the transfer: for each payment the account made, it looks for a path back from the beneficiary over shared identifiers and devices. An empty <code class="language-plaintext highlighter-rouge">ring</code> means the two ends are not otherwise connected.</p>

<p>To rank an account’s neighbors by how much they share with it, count the identifiers per neighbor:</p>

<div class="code-tabs">
  <div class="code-tabs-nav" role="tablist" aria-label="Shared identifier query language">
    <button class="code-tab active" role="tab" data-lang="cypher" aria-selected="true" tabindex="0">Cypher</button>
    <button class="code-tab" role="tab" data-lang="sql" aria-selected="false" tabindex="-1">SQL</button>
  </div>

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

    <div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">a:</span><span class="n">Account</span><span class="ss">)</span><span class="o">-</span><span class="ss">[</span><span class="nc">:CLAIMS</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">(</span><span class="py">i:</span><span class="n">Identifier</span><span class="ss">)</span><span class="o">&lt;-</span><span class="ss">[</span><span class="nc">:CLAIMS</span><span class="ss">]</span><span class="o">-</span><span class="ss">(</span><span class="py">b:</span><span class="n">Account</span><span class="ss">)</span>
<span class="k">WHERE</span> <span class="n">a.id</span> <span class="o">=</span> <span class="n">$accountId</span> <span class="ow">AND</span> <span class="n">a</span> <span class="o">&lt;&gt;</span> <span class="n">b</span>
<span class="k">RETURN</span> <span class="n">b.id</span> <span class="k">AS</span> <span class="n">account</span><span class="ss">,</span> <span class="nf">count</span><span class="ss">(</span><span class="n">i</span><span class="ss">)</span> <span class="k">AS</span> <span class="n">sharedCount</span><span class="ss">,</span> <span class="nf">collect</span><span class="ss">(</span><span class="n">i.type</span><span class="ss">)</span> <span class="k">AS</span> <span class="n">shared</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">sharedCount</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </div>

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

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">id</span> <span class="k">AS</span> <span class="n">account</span><span class="p">,</span> <span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">sharedCount</span>
<span class="k">FROM</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">expand</span><span class="p">(</span><span class="k">out</span><span class="p">(</span><span class="s1">'CLAIMS'</span><span class="p">).</span><span class="k">in</span><span class="p">(</span><span class="s1">'CLAIMS'</span><span class="p">))</span> <span class="k">FROM</span> <span class="n">Account</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="p">:</span><span class="n">accountId</span><span class="p">)</span>
<span class="k">WHERE</span> <span class="n">id</span> <span class="o">&lt;&gt;</span> <span class="p">:</span><span class="n">accountId</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">id</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">sharedCount</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </div>
</div>

<p>Two accounts sharing one identifier is weak evidence. Two accounts sharing four is not. The count is the signal, and grouping it out of the traversal costs nothing.</p>

<h2 id="the-ring-alone-is-not-enough">The ring alone is not enough</h2>

<p>Graph database marketing tends to skip this part, and it is why a single ring query is not a fraud system.</p>

<p><strong>A ring is not proof.</strong> Five accounts sharing an address might be a family, roommates, or a small business. Five accounts sharing a device might be a library. The traversal finds the pattern; it does not tell you the pattern is fraud. Run it alone and you will generate false positives at a rate your review team will hate you for.</p>

<p>Confidence comes from combining the structural signal with signals of a different kind: text similarity, behavior, and timing. ArcadeDB keeps each of those in the same database as the graph.</p>

<h3 id="fuzzy-identity-matching-for-synthetics">Fuzzy identity matching, for synthetics</h3>

<p>Synthetic identities are near-misses by construction. <code class="language-plaintext highlighter-rouge">123 Main St Apt 4</code> and <code class="language-plaintext highlighter-rouge">123 Main Street #4</code>. <code class="language-plaintext highlighter-rouge">Jonathon</code> and <code class="language-plaintext highlighter-rouge">Johnathon</code>. Exact matching finds none of it.</p>

<p>A full-text index, which ArcadeDB scores with BM25, ranks the near-matches. The index is created once, in SQL:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="n">PROPERTY</span> <span class="n">Identifier</span><span class="p">.</span><span class="n">value</span> <span class="n">STRING</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="k">ON</span> <span class="n">Identifier</span> <span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="n">FULL_TEXT</span><span class="p">;</span>
</code></pre></div></div>

<p>Then query it from either language:</p>

<div class="code-tabs">
  <div class="code-tabs-nav" role="tablist" aria-label="Full-text query language">
    <button class="code-tab active" role="tab" data-lang="cypher" aria-selected="true" tabindex="0">Cypher</button>
    <button class="code-tab" role="tab" data-lang="sql" aria-selected="false" tabindex="-1">SQL</button>
  </div>

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

    <div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CALL</span> <span class="n">db.index.fulltext.queryNodes</span><span class="ss">(</span><span class="s1">'Identifier[value]'</span><span class="ss">,</span> <span class="s1">'"123 Main" Apt~'</span><span class="ss">)</span>
<span class="k">YIELD</span> <span class="n">node</span><span class="ss">,</span> <span class="n">score</span>
<span class="k">RETURN</span> <span class="n">node.value</span> <span class="k">AS</span> <span class="n">value</span><span class="ss">,</span> <span class="n">node.type</span> <span class="k">AS</span> <span class="n">type</span><span class="ss">,</span> <span class="n">score</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">score</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </div>

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

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">value</span><span class="p">,</span> <span class="k">type</span><span class="p">,</span> <span class="err">$</span><span class="n">score</span> <span class="k">AS</span> <span class="n">score</span>
<span class="k">FROM</span> <span class="n">Identifier</span>
<span class="k">WHERE</span> <span class="n">SEARCH_INDEX</span><span class="p">(</span><span class="s1">'Identifier[value]'</span><span class="p">,</span> <span class="s1">'"123 Main" Apt~'</span><span class="p">)</span> <span class="o">=</span> <span class="k">true</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">score</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </div>
</div>

<p>One near-match proves nothing on its own. A cluster of applications sharing near-miss identifiers, all filed within a few days of each other, is a pattern exact matching cannot see at all.</p>

<h3 id="behavioral-distance-for-bust-outs">Behavioral distance, for bust-outs</h3>

<p>An account that has behaved normally for eight months and then changes character is the classic bust-out. If you already embed account behavior as a vector, and many teams do, the change shows up as distance: recently active accounts whose behavior now sits close to a known fraud profile.</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">Account</span> <span class="p">(</span><span class="n">behavior</span><span class="p">)</span> <span class="n">LSM_VECTOR</span> <span class="n">METADATA</span> <span class="p">{</span><span class="n">dimensions</span><span class="p">:</span> <span class="mi">128</span><span class="p">,</span> <span class="n">similarity</span><span class="p">:</span> <span class="s1">'COSINE'</span><span class="p">};</span>
</code></pre></div></div>

<div class="code-tabs">
  <div class="code-tabs-nav" role="tablist" aria-label="Vector query language">
    <button class="code-tab active" role="tab" data-lang="cypher" aria-selected="true" tabindex="0">Cypher</button>
    <button class="code-tab" role="tab" data-lang="sql" aria-selected="false" tabindex="-1">SQL</button>
  </div>

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

    <div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CALL</span> <span class="n">db.index.vector.queryNodes</span><span class="ss">(</span><span class="s1">'Account[behavior]'</span><span class="ss">,</span> <span class="mi">20</span><span class="ss">,</span> <span class="n">$knownFraudVector</span><span class="ss">)</span>
<span class="k">YIELD</span> <span class="n">node</span><span class="ss">,</span> <span class="n">score</span>
<span class="k">WHERE</span> <span class="n">node.lastActivity</span> <span class="o">&gt;</span> <span class="n">$since</span>
<span class="k">RETURN</span> <span class="n">node.id</span> <span class="k">AS</span> <span class="n">id</span><span class="ss">,</span> <span class="n">score</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">score</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </div>

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

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">id</span><span class="p">,</span> <span class="n">distance</span>
<span class="k">FROM</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">expand</span><span class="p">(</span><span class="n">vectorNeighbors</span><span class="p">(</span><span class="s1">'Account[behavior]'</span><span class="p">,</span> <span class="p">:</span><span class="n">knownFraudVector</span><span class="p">,</span> <span class="mi">20</span><span class="p">)))</span>
<span class="k">WHERE</span> <span class="n">lastActivity</span> <span class="o">&gt;</span> <span class="p">:</span><span class="n">since</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">distance</span>
</code></pre></div>    </div>

  </div>
</div>

<p>Cypher reports a similarity <code class="language-plaintext highlighter-rouge">score</code>, where higher is closer. SQL reports a <code class="language-plaintext highlighter-rouge">distance</code>, where lower is closer. Both rank the same neighbors.</p>

<p>This signal is independent of the graph one. An account can be structurally isolated and behaviorally anomalous, or structurally suspicious and behaviorally boring. The accounts that are both are the ones to look at first.</p>

<h3 id="timing-for-velocity">Timing, for velocity</h3>

<p>Fraud has a tempo. Ten transfers in a minute, or a burst of account creations from one subnet, is a question of bucketing events by time:</p>

<div class="code-tabs">
  <div class="code-tabs-nav" role="tablist" aria-label="Velocity query language">
    <button class="code-tab active" role="tab" data-lang="cypher" aria-selected="true" tabindex="0">Cypher</button>
    <button class="code-tab" role="tab" data-lang="sql" aria-selected="false" tabindex="-1">SQL</button>
  </div>

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

    <div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">a:</span><span class="n">Account</span><span class="ss">)</span><span class="o">-</span><span class="ss">[</span><span class="py">t:</span><span class="n">TRANSFERRED</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">()</span>
<span class="k">WHERE</span> <span class="n">t.ts</span> <span class="o">&gt;</span> <span class="n">datetime</span><span class="ss">(</span><span class="n">$since</span><span class="ss">)</span>
<span class="k">WITH</span> <span class="n">a.id</span> <span class="k">AS</span> <span class="n">account_id</span><span class="ss">,</span>
     <span class="n">datetime.truncate</span><span class="ss">(</span><span class="s1">'minute'</span><span class="ss">,</span> <span class="n">t.ts</span><span class="ss">)</span> <span class="k">AS</span> <span class="n">minute</span><span class="ss">,</span>
     <span class="nf">count</span><span class="ss">(</span><span class="n">t</span><span class="ss">)</span> <span class="k">AS</span> <span class="n">transfers</span><span class="ss">,</span>
     <span class="nf">sum</span><span class="ss">(</span><span class="n">t.amount</span><span class="ss">)</span> <span class="k">AS</span> <span class="n">total</span>
<span class="k">WHERE</span> <span class="n">transfers</span> <span class="o">&gt;</span> <span class="mi">10</span>
<span class="k">RETURN</span> <span class="n">minute</span><span class="ss">,</span> <span class="n">account_id</span><span class="ss">,</span> <span class="n">transfers</span><span class="ss">,</span> <span class="n">total</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">total</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </div>

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

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="n">ts</span><span class="p">.</span><span class="n">timeBucket</span><span class="p">(</span><span class="s1">'1m'</span><span class="p">,</span> <span class="n">ts</span><span class="p">)</span> <span class="k">AS</span> <span class="k">minute</span><span class="p">,</span>
         <span class="n">account_id</span><span class="p">,</span>
         <span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">transfers</span><span class="p">,</span>
         <span class="k">sum</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span> <span class="k">AS</span> <span class="n">total</span>
  <span class="k">FROM</span> <span class="n">TransferEvent</span>
  <span class="k">WHERE</span> <span class="n">ts</span> <span class="o">&gt;</span> <span class="p">:</span><span class="n">since</span>
  <span class="k">GROUP</span> <span class="k">BY</span> <span class="k">minute</span><span class="p">,</span> <span class="n">account_id</span>
<span class="p">)</span>
<span class="k">WHERE</span> <span class="n">transfers</span> <span class="o">&gt;</span> <span class="mi">10</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">total</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </div>
</div>

<p>The two versions read different data. Cypher works on the graph, so it buckets the timestamps already on the <code class="language-plaintext highlighter-rouge">TRANSFERRED</code> edges. SQL reads <code class="language-plaintext highlighter-rouge">TransferEvent</code>, a time-series type, where the storage engine can answer a bucketed aggregation in one pass. That is the better home for a high-volume event stream, and Cypher does not query time-series types.</p>

<h2 id="why-one-database-rather-than-four">Why one database rather than four</h2>

<p>Every signal above is weak on its own and meaningful in combination, and that has consequences for where the data lives.</p>

<p>If the graph lives in one system, the embeddings in a vector store, the identifiers in a search cluster, and the events in a time-series database, then combining them means four queries, four network round trips, a join written in application code, and four copies of the truth that drift at four different rates. Worse, the drift is invisible: nothing errors when your vector store is nine minutes behind your graph. You just score an account against a behavior profile that is no longer current.</p>

<p>With all four models in one engine, the join happens in the query and every signal reads the same committed data. That is a narrower claim than “one database for everything”. It applies here because fraud scoring combines these signals on every query, so keeping them apart costs you on every query too.</p>

<h2 id="where-a-traversal-is-the-wrong-tool">Where a traversal is the wrong tool</h2>

<p>A graph is not the answer to everything, and two limits are worth knowing before you start.</p>

<p><strong>Unbounded traversals will hurt you.</strong> <code class="language-plaintext highlighter-rouge">[:TRANSFERRED*]</code> with no upper bound on a dense graph can walk a very large portion of it. Every query above bounds the depth deliberately. Bound yours too, and exclude hub vertices by hand. A bank’s own settlement account touches everything; the engine will not work that out for you.</p>

<p><strong>Whole-graph analytics is a different workload.</strong> Community detection or PageRank across the entire account graph is not a transactional traversal, and running it as one will disappoint you. That is what the <a href="/blog/graph-olap-engine-the-fastest-graph-analytics-with-zero-compromises/">Graph OLAP engine</a> is for. It reads the same data as the transactional engine, so there is no export step between finding a community offline and querying it online.</p>

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

<p>The schema above is six statements and the Cypher ring query is three lines. If you want the longer version, with the multi-model investigation flow and the FAQ, it is on the <a href="/fraud-detection.html">fraud detection page</a>; the <a href="/academy.html">Academy</a> covers the Cypher and SQL used here in more depth.</p>

<p>The FTC recorded $12.5 billion in consumer fraud losses in 2024, a 25% increase year over year, and the FBI’s Internet Crime Complaint Center put its own figure at $16.6 billion for the same period. A lot of that money moved through accounts that looked fine one row at a time.</p>]]></content><author><name>Roberto Franchini</name></author><category term="Fraud Detection" /><category term="Graph Database" /><category term="Cypher" /><category term="Vector Search" /><category term="Full-Text Search" /><category term="Time Series" /><category term="Multi-Model" /><category term="AML" /><category term="ArcadeDB" /><summary type="html"><![CDATA[Fraud rings, synthetic identities, and money laundering are connection problems, not row problems. How to model and query all three in ArcadeDB with graph traversal, vector similarity, full-text matching, and time series, in one database.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/fraud-detection-graph-rings.png" /><media:content medium="image" url="https://arcadedb.com/assets/images/fraud-detection-graph-rings.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB 26.10.2: Query Admission Gate, Terminate Running Queries, Faster Point Lookups, and Native Cypher Temporals</title><link href="https://arcadedb.com/blog/arcadedb-26-10-2/" rel="alternate" type="text/html" title="ArcadeDB 26.10.2: Query Admission Gate, Terminate Running Queries, Faster Point Lookups, and Native Cypher Temporals" /><published>2026-10-15T00:00:00+00:00</published><updated>2026-10-15T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-26-10-2</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-26-10-2/"><![CDATA[<p><strong>ArcadeDB 26.10.2</strong> closes <strong>682 issues and pull requests</strong> under the <a href="https://github.com/ArcadeData/arcadedb/milestone/62">26.10.2 milestone</a>: 485 issues and 197 PRs, out of 355 pull requests merged and about <strong>1,360 commits</strong> since <a href="https://arcadedb.com/blog/arcadedb-26-10-1/">26.10.1</a>. <strong>377 of the closed issues are bug fixes, 20 of them rated critical.</strong></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>Query control</td>
      <td>A JVM-wide admission gate, plus list and terminate for running queries on every protocol</td>
      <td>on by default</td>
    </tr>
    <tr>
      <td>Query speed</td>
      <td>SQL and openCypher statements that differ only in their literals share one parsed statement and one plan</td>
      <td>point lookup <strong>3 to 7x</strong> faster</td>
    </tr>
    <tr>
      <td>High availability (Raft)</td>
      <td>Schema and setting changes reach every node, vectors on followers stop replacing each other, a node removed from the cluster is fenced</td>
      <td>follower refusals are now <strong>redirects</strong></td>
    </tr>
    <tr>
      <td>Time series and Prometheus</td>
      <td>Retention and compaction no longer corrupt or resurrect data in the sealed store</td>
      <td>PromQL follows Prometheus on a dozen edge cases</td>
    </tr>
    <tr>
      <td>openCypher</td>
      <td>Temporals stored natively, <code class="language-plaintext highlighter-rouge">MERGE</code> matches what <code class="language-plaintext highlighter-rouge">MATCH</code> matches, <code class="language-plaintext highlighter-rouge">path.*</code> answers like APOC</td>
      <td><strong>4</strong> new property types</td>
    </tr>
    <tr>
      <td>Security</td>
      <td>Open sessions re-check the user, nested statements are classified, Studio no longer runs HTML from an AI answer</td>
      <td>see <a href="#security">Security</a></td>
    </tr>
    <tr>
      <td>Silent wrong data</td>
      <td>Lost keys during a commit, duplicated RIDs after a schema reload, schema changes lost at close</td>
      <td>all closed</td>
    </tr>
  </tbody>
</table>

<p><strong>Upgrading is recommended for every deployment, and in particular for every HA cluster and every time series workload.</strong> Behavior changes are collected in the <a href="#upgrade-checklist">upgrade checklist</a> below.</p>

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

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

<ul>
  <li><strong>Query admission gate</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9518">#9518</a>). A FIFO queue in front of HTTP, Postgres, Bolt, Redis, MongoDB, gRPC and MCP: when the server is busy, requests wait and start in arrival order instead of all starting at once and exhausting the heap. It is on by default (<code class="language-plaintext highlighter-rouge">arcadedb.queryMaxConcurrent</code>, 2 x cores and at least 4, <code class="language-plaintext highlighter-rouge">0</code> disables it), bounded by <code class="language-plaintext highlighter-rouge">arcadedb.queryQueueTimeout</code> and <code class="language-plaintext highlighter-rouge">arcadedb.queryQueueMaxSize</code>, and heap-aware through <code class="language-plaintext highlighter-rouge">arcadedb.queryAdmissionHeapWatermark</code>. A refused request is retryable on every protocol (HTTP 503, Postgres <code class="language-plaintext highlighter-rouge">40001</code>, Redis <code class="language-plaintext highlighter-rouge">TRYAGAIN</code>).</li>
  <li><strong>List and terminate running queries</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9680">#9680</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9689">#9689</a>). <code class="language-plaintext highlighter-rouge">list queries [database &lt;db&gt;] [tag &lt;t&gt;]</code>, <code class="language-plaintext highlighter-rouge">terminate query &lt;id&gt;</code>, <code class="language-plaintext highlighter-rouge">terminate queries tag &lt;t&gt;</code>, <code class="language-plaintext highlighter-rouge">list transactions</code> and <code class="language-plaintext highlighter-rouge">terminate transaction &lt;sessionId&gt;</code> over <code class="language-plaintext highlighter-rouge">POST /api/v1/server</code>, with an <code class="language-plaintext highlighter-rouge">X-ArcadeDB-Query-Tag</code> header to find your own statements and an acknowledgement that says whether the statement stopped. Bolt, gRPC (streaming loads included), MCP, the Postgres wire and <code class="language-plaintext highlighter-rouge">/ws</code> insert sessions register their work too, and every entry names its client (<a href="https://github.com/ArcadeData/arcadedb/issues/9774">#9774</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9838">#9838</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9875">#9875</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9962">#9962</a>).</li>
  <li><strong>Statement and plan cache by query shape</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/8307">#8307</a>). SQL and openCypher texts that differ only in their literals are parsed and planned once. On a unique-index point lookup, Cypher drops from 42 to 6.1 microseconds per query and SQL from 21.5 to 6.6; a query that already uses parameters costs what it did.</li>
  <li><strong>Docker images run on Java 25</strong> (<code class="language-plaintext highlighter-rouge">eclipse-temurin:25-alpine</code>), so compact object headers are finally enabled in the image (<a href="https://github.com/ArcadeData/arcadedb/issues/9602">#9602</a>). The <strong>native image</strong> now ships Raft HA, metrics and tracing (<a href="https://github.com/ArcadeData/arcadedb/issues/9407">#9407</a>), runs the e2e battery in CI (<a href="https://github.com/ArcadeData/arcadedb/issues/9495">#9495</a>), and uses scalar vector math instead of the uncompiled Vector API, which was 6 to 27 times slower (<a href="https://github.com/ArcadeData/arcadedb/issues/10072">#10072</a>).</li>
  <li><strong>Studio</strong> is served with a <strong>Content-Security-Policy</strong> that refuses inline script (<a href="https://github.com/ArcadeData/arcadedb/issues/9681">#9681</a>), and the saved-query and history sidebar, the Reference panel, the sample-database cards, the API endpoint list and the Profiler rows are keyboard accessible (<a href="https://github.com/ArcadeData/arcadedb/issues/8740">#8740</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9375">#9375</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9508">#9508</a>).</li>
  <li><strong>HA operations</strong>: an operator override to lift a quarantine or the stale-snapshot read floor on a <strong>sole voter</strong> (<code class="language-plaintext highlighter-rouge">POST /api/v1/cluster/accept-diverged/{database}</code>) (<a href="https://github.com/ArcadeData/arcadedb/issues/9449">#9449</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9498">#9498</a>); <code class="language-plaintext highlighter-rouge">GET /api/v1/cluster</code> exposes the follower’s replication-path and unreachable-leader state and in-place restart counters, with matching alerts (<a href="https://github.com/ArcadeData/arcadedb/issues/9399">#9399</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9429">#9429</a>); an alert when every voter has quarantined the same database (<a href="https://github.com/ArcadeData/arcadedb/issues/9553">#9553</a>); the remaining Raft executors published as pool metrics (<a href="https://github.com/ArcadeData/arcadedb/issues/8856">#8856</a>); and a configurable DEFLATE level for snapshot ship (<code class="language-plaintext highlighter-rouge">arcadedb.ha.snapshotCompressionLevel</code>) (<a href="https://github.com/ArcadeData/arcadedb/pull/9295">#9295</a>).</li>
  <li><strong>Backups</strong>: SQL <code class="language-plaintext highlighter-rouge">BACKUP DATABASE</code> applies the retention policy (<a href="https://github.com/ArcadeData/arcadedb/issues/9844">#9844</a>), and a full backup archives a vector index’s graph consistently so a restore reuses it (<a href="https://github.com/ArcadeData/arcadedb/issues/9917">#9917</a>).</li>
</ul>

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

<ul>
  <li><strong>Native openCypher temporals</strong>: four new property types, <code class="language-plaintext highlighter-rouge">OFFSET_TIME</code>, <code class="language-plaintext highlighter-rouge">LOCAL_TIME</code>, <code class="language-plaintext highlighter-rouge">ZONED_DATETIME</code> and <code class="language-plaintext highlighter-rouge">DURATION</code>, indexable by <code class="language-plaintext highlighter-rouge">LSM_TREE</code> and <code class="language-plaintext highlighter-rouge">HASH</code>. A String is never reinterpreted as a time again (<a href="https://github.com/ArcadeData/arcadedb/issues/8572">#8572</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9462">#9462</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9636">#9636</a>).</li>
  <li><strong>Customizable Contraction Hierarchies</strong>: a Graph Analytical View keeps a CCH per weight property (<code class="language-plaintext highlighter-rouge">CREATE GRAPH ANALYTICAL VIEW ... CCH (distance)</code>), so weighted point-to-point shortest paths no longer depend on the distance (<code class="language-plaintext highlighter-rouge">cchShortestPath()</code>, <code class="language-plaintext highlighter-rouge">algo.cch.shortestPath</code>), and the hierarchy keeps up with weight updates in milliseconds (<a href="https://github.com/ArcadeData/arcadedb/issues/9437">#9437</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9915">#9915</a>). This is the building block for <a href="https://arcadedb.com/route-planning.html">route planning</a> on large road and logistics graphs.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">path.*</code> procedures answer as <code class="language-plaintext highlighter-rouge">apoc.path.*</code></strong>: APOC’s filter syntax, nine <code class="language-plaintext highlighter-rouge">uniqueness</code> modes, <code class="language-plaintext highlighter-rouge">endNodes</code>, <code class="language-plaintext highlighter-rouge">terminatorNodes</code>, <code class="language-plaintext highlighter-rouge">allowlistNodes</code>, <code class="language-plaintext highlighter-rouge">denylistNodes</code>, <code class="language-plaintext highlighter-rouge">optional</code>, a lazy walk that a <code class="language-plaintext highlighter-rouge">limit</code> really stops, and a heap-charged breadth-first walk (<a href="https://github.com/ArcadeData/arcadedb/issues/10025">#10025</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10048">#10048</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10049">#10049</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10017">#10017</a>). Teams moving from Neo4j can check how much of the surface is covered on the <a href="https://arcadedb.com/neo4j.html">Neo4j alternative page</a>.</li>
  <li><strong>openCypher label expressions on variable-length relationships</strong> (<code class="language-plaintext highlighter-rouge">[:!R*]</code>, <code class="language-plaintext highlighter-rouge">[:%*]</code>, <code class="language-plaintext highlighter-rouge">shortestPath((a)-[:!R*]-(b))</code>) now run (<a href="https://github.com/ArcadeData/arcadedb/issues/9117">#9117</a>).</li>
  <li><strong>SQL</strong>: <code class="language-plaintext highlighter-rouge">CREATE INDEX ... METADATA {"buildMode": "SORTED"}</code> selects the sorted build (<a href="https://github.com/ArcadeData/arcadedb/issues/9291">#9291</a>); <code class="language-plaintext highlighter-rouge">CREATE GRAPH ANALYTICAL VIEW v PROPERTIES ()</code> builds a view with no vertex properties (<a href="https://github.com/ArcadeData/arcadedb/issues/9831">#9831</a>); <code class="language-plaintext highlighter-rouge">algo.labelpropagation</code> takes a <code class="language-plaintext highlighter-rouge">tieBreakProperty</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9285">#9285</a>); <code class="language-plaintext highlighter-rouge">CHECK DATABASE</code> reports unreadable stripe directories (<a href="https://github.com/ArcadeData/arcadedb/issues/9266">#9266</a>); Gremlin <code class="language-plaintext highlighter-rouge">hasLabel</code> sees types created earlier in the same traversal (<a href="https://github.com/ArcadeData/arcadedb/issues/9335">#9335</a>).</li>
  <li><strong>PromQL</strong>: the <code class="language-plaintext highlighter-rouge">bool</code> comparison modifier (<code class="language-plaintext highlighter-rouge">a &gt; bool 1</code>) (<a href="https://github.com/ArcadeData/arcadedb/issues/9644">#9644</a>).</li>
</ul>

<h3 id="remote-clients">Remote Clients</h3>

<ul>
  <li><strong>Typed “already exists” exceptions</strong>: the Java remote clients raise <code class="language-plaintext highlighter-rouge">RemoteAlreadyExistsException</code> for a taken user, token or database name, and the server answers <strong>409</strong> for them over HTTP (<a href="https://github.com/ArcadeData/arcadedb/issues/10073">#10073</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10074">#10074</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10075">#10075</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">RemoteSchema.alterMaterializedView()</code> is implemented, and a streamed error line rebuilds its typed exception (<a href="https://github.com/ArcadeData/arcadedb/issues/8726">#8726</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8874">#8874</a>).</li>
</ul>

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

<h3 id="high-availability-schema-settings-and-recovery-reach-every-node">High Availability: Schema, Settings and Recovery Reach Every Node</h3>

<p>If you run ArcadeDB in <a href="https://arcadedb.com/client-server.html">client-server mode</a> with a Raft cluster, this release continues the hardening pass that started in 26.10.1.</p>

<ul>
  <li><strong>Changes that were saved on the leader only now replicate.</strong> <code class="language-plaintext highlighter-rouge">ALTER PROPERTY</code> constraints and the <code class="language-plaintext highlighter-rouge">Property</code> setters, schema extensions, the <code class="language-plaintext highlighter-rouge">ALTER DATABASE</code> date formats and <code class="language-plaintext highlighter-rouge">setZoneId</code>, <code class="language-plaintext highlighter-rouge">ALTER TYPE ... WITH lightweight / unique</code>, the time series downsampling policy, <code class="language-plaintext highlighter-rouge">ALTER TYPE ... NAME</code> (type, bucket, index and edge files), <code class="language-plaintext highlighter-rouge">ALTER DATABASE</code> / <code class="language-plaintext highlighter-rouge">set database setting</code>, a JSONL import’s triggers, views, continuous aggregates and functions, and <code class="language-plaintext highlighter-rouge">db.migrateLegacyTemporals()</code>. A constraint went unenforced on a node elected after a failover, and date settings answered differently per node (<a href="https://github.com/ArcadeData/arcadedb/issues/9747">#9747</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9762">#9762</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9763">#9763</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9764">#9764</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9712">#9712</a>). During a rolling upgrade a rename or a setting change is refused with <strong>409</strong> until every node understands it.</li>
  <li><strong>Vectors inserted on a follower replaced other vectors.</strong> A replicated vector index page did not move the follower’s id allocator, so the next insert reused an id the leader had allocated: <code class="language-plaintext highlighter-rouge">countEntries()</code> stayed flat and the superseded vectors dropped out of search (<a href="https://github.com/ArcadeData/arcadedb/issues/9428">#9428</a>). A follower’s vector graph also picks up vectors replicated after its first search (<a href="https://github.com/ArcadeData/arcadedb/issues/9857">#9857</a>, fixed by <a href="https://github.com/blaus23">@blaus23</a> in <a href="https://github.com/ArcadeData/arcadedb/pull/9859">#9859</a>), and a snapshot install or a full backup no longer reuses a vector graph without the manifest that vouches for it (<a href="https://github.com/ArcadeData/arcadedb/issues/9758">#9758</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9888">#9888</a>). Anyone running the <a href="https://arcadedb.com/graph-rag.html">GraphRAG stack</a> on a cluster should take this fix.</li>
  <li><strong>No leader after a leader loss in a 3-node cluster.</strong> Once a stopped node was gone, its host name could resolve to the node asking for it, so every Raft message meant for the dead node was delivered back to the sender and answered as if it were that node: the new leader received its own heartbeat and stepped down, and every election then counted its own vote twice. A node now refuses any Raft message addressed to another node and reconnects to a peer when it notices it has messaged itself (<a href="https://github.com/ArcadeData/arcadedb/issues/10016">#10016</a>, reported on a real cluster). A closed state machine refuses commits at once instead of waiting the quorum timeout (<a href="https://github.com/ArcadeData/arcadedb/issues/9411">#9411</a>), the Ratis close is bounded on <code class="language-plaintext highlighter-rouge">stop()</code> and on an in-place restart (<a href="https://github.com/ArcadeData/arcadedb/issues/9561">#9561</a>), and a log close can no longer deadlock with an in-flight cache eviction (<a href="https://github.com/ArcadeData/arcadedb/issues/9556">#9556</a>).</li>
  <li><strong>An in-place Ratis restart double-applied a commit</strong>, and several replicated commit paths never fired after-commit callbacks, which also lost Redis transactions that only changed in-memory values (<a href="https://github.com/ArcadeData/arcadedb/issues/8785">#8785</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8784">#8784</a>).</li>
  <li><strong>Two committed schema changes gave the same file id to different compacted index files</strong>, quarantining the database on every node (<a href="https://github.com/ArcadeData/arcadedb/issues/9547">#9547</a>). A restarted node ran out of heap catching up about 61,000 Raft entries on a 1 GB heap; the entry cache is now bound to the heap (<a href="https://github.com/ArcadeData/arcadedb/issues/9549">#9549</a>).</li>
  <li><strong>A node removed from the Raft configuration</strong> no longer accepts writes that stall for the quorum timeout, and refuses consistent reads, drops and installs (<a href="https://github.com/ArcadeData/arcadedb/issues/9510">#9510</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9590">#9590</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9589">#9589</a>).</li>
  <li><strong>The proposer of a refused schema entry is quarantined and resynced</strong> instead of staying diverged, and a bootstrap fingerprint entry applied during shutdown is left for replay (<a href="https://github.com/ArcadeData/arcadedb/issues/9555">#9555</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9558">#9558</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9568">#9568</a>). A sole voter no longer quarantines a database on an unexpected apply error (<a href="https://github.com/ArcadeData/arcadedb/issues/9308">#9308</a>).</li>
  <li><strong>A schema change no longer breaks time series writes on followers</strong>: every schema reload closed the files of each time series type that held data, so the next write failed and the follower resynced (<a href="https://github.com/ArcadeData/arcadedb/issues/9886">#9886</a>).</li>
  <li><strong>A follower’s refusal of leader-only work is answered at once</strong> (<code class="language-plaintext highlighter-rouge">Database.transaction()</code> used to retry it through every <code class="language-plaintext highlighter-rouge">txRetries</code> attempt) and always names the leader, never the refusing node (<a href="https://github.com/ArcadeData/arcadedb/issues/9995">#9995</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10066">#10066</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10060">#10060</a>). On the wire this is now a redirect: Bolt <code class="language-plaintext highlighter-rouge">Neo.ClientError.Cluster.NotALeader</code>, MongoDB <code class="language-plaintext highlighter-rouge">10107 NotWritablePrimary</code>, Redis <code class="language-plaintext highlighter-rouge">READONLY</code>, and <code class="language-plaintext highlighter-rouge">NOT_LEADER</code> with the leader’s address on <code class="language-plaintext highlighter-rouge">/ws</code> and gRPC.</li>
  <li><strong>The cluster credential on every peer dial</strong> is built in one place (<code class="language-plaintext highlighter-rouge">PeerRequest</code>), so the token and forwarded user always travel together (<a href="https://github.com/ArcadeData/arcadedb/issues/9999">#9999</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10068">#10068</a>).</li>
</ul>

<h3 id="lost-updates-and-silent-data-loss">Lost Updates and Silent Data Loss</h3>

<ul>
  <li><strong>Index lookups lost a key while a commit published</strong>: a point lookup, <code class="language-plaintext highlighter-rouge">Database.lookupByKey</code>, <code class="language-plaintext highlighter-rouge">pid IN :ids</code> in SQL and in openCypher missed (11 to 23 percent of reads in the reproduction) or served another key’s record when a transaction deleted and re-created a key (<a href="https://github.com/ArcadeData/arcadedb/issues/9369">#9369</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9397">#9397</a>).</li>
  <li><strong>A schema reload issued the same new page and RID to two transactions</strong>, because the reloaded bucket started with an empty page-reservation state (<a href="https://github.com/ArcadeData/arcadedb/issues/9835">#9835</a>).</li>
  <li><strong>A schema change pending at close was lost</strong> whenever the schema held an index: the save ran after the database reported itself closed, failed at <code class="language-plaintext highlighter-rouge">SEVERE</code> and left the type or index gone after a reopen (<a href="https://github.com/ArcadeData/arcadedb/issues/10059">#10059</a>).</li>
  <li><strong>LSM index compaction reset the null strategy to <code class="language-plaintext highlighter-rouge">SKIP</code></strong> for <code class="language-plaintext highlighter-rouge">ERROR</code> and <code class="language-plaintext highlighter-rouge">INDEX</code> indexes, and saved it to <code class="language-plaintext highlighter-rouge">schema.json</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9717">#9717</a>).</li>
  <li><strong>WAL recovery set the next transaction id from the last replayed transaction, not the highest</strong>, so after a crash with two concurrent commits the database re-issued transaction ids (<a href="https://github.com/ArcadeData/arcadedb/issues/9795">#9795</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">include()</code> / <code class="language-plaintext highlighter-rouge">exclude()</code> returned a dirty record stamped with the source RID</strong>, so saving a projection truncated that record and orphaned its edges (<a href="https://github.com/ArcadeData/arcadedb/issues/9840">#9840</a>). <code class="language-plaintext highlighter-rouge">INSERT ... FROM SELECT *, !secret</code> copied the excluded column (<a href="https://github.com/ArcadeData/arcadedb/issues/9965">#9965</a>).</li>
  <li><strong>A MongoDB insert made while the caller held a transaction committed its own and rolled the caller’s back</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9977">#9977</a>).</li>
  <li><strong>A refused <code class="language-plaintext highlighter-rouge">DELETE_RECORD</code> had already removed the record’s index entries, EXTERNAL values and edges</strong>; the check now runs first (<a href="https://github.com/ArcadeData/arcadedb/issues/9305">#9305</a>). An edge iterator’s <code class="language-plaintext highlighter-rouge">remove()</code> unlinked an edge the user may not delete (<a href="https://github.com/ArcadeData/arcadedb/issues/9779">#9779</a>).</li>
  <li><strong>A multi-page record could be read torn</strong> under <code class="language-plaintext highlighter-rouge">REPEATABLE_READ</code> after a concurrent rewrite (<a href="https://github.com/ArcadeData/arcadedb/issues/9070">#9070</a>), a flush suspension released on the wrong thread left every later backup, freeze and close timing out (<a href="https://github.com/ArcadeData/arcadedb/issues/9832">#9832</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9927">#9927</a>), and an async batch on a one-bucket type lost 1,000 records in 24 of 6,000 runs until its replay waited a jittered backoff (<a href="https://github.com/ArcadeData/arcadedb/issues/9529">#9529</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9694">#9694</a>).</li>
  <li><strong>The Python driver</strong> rolls back after an interrupt in <code class="language-plaintext highlighter-rouge">begin()</code>, and <code class="language-plaintext highlighter-rouge">run_in_transaction</code> retries a <code class="language-plaintext highlighter-rouge">LockTimeoutException</code>, where 193 failures escaped in the reproduction (<a href="https://github.com/ArcadeData/arcadedb/issues/9321">#9321</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9629">#9629</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9970">#9970</a>). See <a href="https://arcadedb.com/python-embedded.html">ArcadeDB embedded in Python</a> if you run the engine in-process.</li>
</ul>

<h3 id="time-series-and-prometheus-the-sealed-store-stops-losing-and-resurrecting-data">Time Series and Prometheus: the Sealed Store Stops Losing and Resurrecting Data</h3>

<p>If you use ArcadeDB for <a href="https://arcadedb.com/realtime-analytics.html">real-time analytics</a> or as a Prometheus remote-write target, this section is the reason to upgrade.</p>

<ul>
  <li><strong>Retention appended its rewritten blocks after whatever an abandoned rewrite left in <code class="language-plaintext highlighter-rouge">.ts.sealed.tmp</code></strong>, so blocks it had just dropped came back on the next open and shipped to every follower (<a href="https://github.com/ArcadeData/arcadedb/issues/9770">#9770</a>). <strong><code class="language-plaintext highlighter-rouge">COMPACT TIMESERIES TYPE</code> racing a retention pass corrupted the live store’s in-memory directory</strong> and silently undid the retention at the next open (<a href="https://github.com/ArcadeData/arcadedb/issues/9771">#9771</a>). Acknowledged rows were missing after a <code class="language-plaintext highlighter-rouge">kill -9</code> during a concurrent compaction (<a href="https://github.com/ArcadeData/arcadedb/issues/9452">#9452</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">PRECISION SECOND</code> lost every sample at the first retention pass</strong>, and a <code class="language-plaintext highlighter-rouge">NANOSECOND</code> type never expired and answered nothing in PromQL, because no timestamp code read the precision (<a href="https://github.com/ArcadeData/arcadedb/issues/9800">#9800</a>).</li>
  <li><strong>Sealed store I/O</strong>: a thread interrupt closed the shared channel for every reader until the next compaction (<a href="https://github.com/ArcadeData/arcadedb/issues/9621">#9621</a>), and a short read dropped every later block of the directory (<a href="https://github.com/ArcadeData/arcadedb/issues/9640">#9640</a>).</li>
  <li><strong>Writes are strict.</strong> An empty tag value, an undeclared tag or field key, a quoted datetime in a typed column and a value the column cannot store are refused or reported as a partial write instead of stored wrong (<a href="https://github.com/ArcadeData/arcadedb/issues/8647">#8647</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8646">#8646</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9809">#9809</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9643">#9643</a>). <code class="language-plaintext highlighter-rouge">arcadedb.timeSeriesUndeclaredKeys</code> restores the lenient behavior for schema-less producers.</li>
  <li><strong>Reads</strong>: an unbounded <code class="language-plaintext highlighter-rouge">SELECT FROM &lt;timeseries type&gt;</code> and the gRPC stream no longer load the whole range (a 50M-point type ran out of heap) (<a href="https://github.com/ArcadeData/arcadedb/issues/9420">#9420</a>), <code class="language-plaintext highlighter-rouge">scanRangeDescending</code> no longer stops early on overlapping blocks (<a href="https://github.com/ArcadeData/arcadedb/issues/9982">#9982</a>), <code class="language-plaintext highlighter-rouge">ts.rank</code> ranks by value and <code class="language-plaintext highlighter-rouge">ts.movingAvg</code> keeps one slot per row (<a href="https://github.com/ArcadeData/arcadedb/issues/9956">#9956</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9958">#9958</a>), an ungrouped <code class="language-plaintext highlighter-rouge">min()</code>/<code class="language-plaintext highlighter-rouge">max()</code> of a <code class="language-plaintext highlighter-rouge">LONG</code> past 2^53 is exact (<a href="https://github.com/ArcadeData/arcadedb/issues/9710">#9710</a>), and <code class="language-plaintext highlighter-rouge">count(*)</code> is answered from block statistics (<a href="https://github.com/ArcadeData/arcadedb/issues/9612">#9612</a>).</li>
  <li><strong>Prometheus</strong>: remote read keeps non-equality matchers and no longer merges series whose labels contain <code class="language-plaintext highlighter-rouge">|</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9803">#9803</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9623">#9623</a>), <code class="language-plaintext highlighter-rouge">/series</code> applies <code class="language-plaintext highlighter-rouge">match[]</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9386">#9386</a>), remote write converts labels into typed tag columns and answers 415 to a 2.0 body (<a href="https://github.com/ArcadeData/arcadedb/issues/9972">#9972</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9624">#9624</a>), and PromQL refuses what Prometheus refuses (<a href="https://github.com/ArcadeData/arcadedb/issues/9723">#9723</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9743">#9743</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9788">#9788</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9883">#9883</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9922">#9922</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9924">#9924</a>).</li>
</ul>

<h3 id="equality-and-typing-agree-everywhere">Equality and Typing Agree Everywhere</h3>

<p>This continues the work from 26.10.1, where the index, SQL and Cypher were made to agree on what “equal” means.</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">MERGE</code> matches exactly what <code class="language-plaintext highlighter-rouge">MATCH</code> matches</strong>: a stored <code class="language-plaintext highlighter-rouge">FLOAT</code>, a temporal against a <code class="language-plaintext highlighter-rouge">STRING</code> property, and <code class="language-plaintext highlighter-rouge">-0.0</code> / <code class="language-plaintext highlighter-rouge">0.0</code> no longer create duplicates or raise a duplicated-key error (<a href="https://github.com/ArcadeData/arcadedb/issues/9693">#9693</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9616">#9616</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9617">#9617</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9618">#9618</a>).</li>
  <li><strong>A pattern finds what it stored in a declared scalar property.</strong> <code class="language-plaintext highlighter-rouge">MERGE (n:S {v: 1})</code> stores <code class="language-plaintext highlighter-rouge">'1'</code> in a <code class="language-plaintext highlighter-rouge">STRING</code> property and used to create a duplicate every time; the same holds for text written into <code class="language-plaintext highlighter-rouge">INTEGER</code>, <code class="language-plaintext highlighter-rouge">LONG</code>, <code class="language-plaintext highlighter-rouge">FLOAT</code>, <code class="language-plaintext highlighter-rouge">DOUBLE</code>, <code class="language-plaintext highlighter-rouge">BOOLEAN</code>, <code class="language-plaintext highlighter-rouge">DATE</code> and <code class="language-plaintext highlighter-rouge">DATETIME</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9695">#9695</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9701">#9701</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9794">#9794</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9935">#9935</a>).</li>
  <li><strong>A <code class="language-plaintext highlighter-rouge">COLLATE ci</code> index no longer changes the answer</strong> of <code class="language-plaintext highlighter-rouge">=</code> / <code class="language-plaintext highlighter-rouge">IN</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9403">#9403</a>), <code class="language-plaintext highlighter-rouge">k = [1, 2]</code> is no longer read as <code class="language-plaintext highlighter-rouge">IN</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9370">#9370</a>), SQL <code class="language-plaintext highlighter-rouge">sum()</code> of an empty group is NULL (<a href="https://github.com/ArcadeData/arcadedb/issues/9351">#9351</a>), and an equality on a <code class="language-plaintext highlighter-rouge">ZONED_DATETIME</code> property agrees with and without an index (<a href="https://github.com/ArcadeData/arcadedb/issues/9648">#9648</a>).</li>
  <li><strong>Weighted shortest-path finders share one edge-weight rule</strong>: a missing weight is 1, and a negative, NaN or infinite weight is not walked, in <code class="language-plaintext highlighter-rouge">dijkstra()</code>, <code class="language-plaintext highlighter-rouge">astar()</code>, <code class="language-plaintext highlighter-rouge">duanSSSP()</code> and every <code class="language-plaintext highlighter-rouge">algo.*</code> finder (<a href="https://github.com/ArcadeData/arcadedb/issues/9443">#9443</a>).</li>
  <li><strong>A statement is classified by everything nested inside it</strong>, so a write hidden in a <code class="language-plaintext highlighter-rouge">LET</code>, a projection or a sub-query is no longer run through the read-only paths (<a href="https://github.com/ArcadeData/arcadedb/issues/9628">#9628</a>).</li>
</ul>

<blockquote>
  <p>A result set that changes after the upgrade is the fix, not a regression.</p>
</blockquote>

<h3 id="faster-queries">Faster Queries</h3>

<p>Besides the plan cache and the admission gate described above, the engine got a round of speed work. For the numbers behind ArcadeDB’s engine speed, see the <a href="https://arcadedb.com/benchmarks.html">benchmarks</a>.</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">GROUP BY</code></strong>: each key is aggregated once in a parallel scan, aggregates are evaluated off bound columns, and the in-heap budget no longer scales with the number of cores (a query that ran on 1 or 2 cores failed on 4 or more) (<a href="https://github.com/ArcadeData/arcadedb/issues/9496">#9496</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9402">#9402</a>).</li>
  <li><strong>Variable-length hops that need only distinct end vertices</strong> walk a visited set instead of every path: <code class="language-plaintext highlighter-rouge">MATCH (a)-[:LINK*1..D]-&gt;(b) RETURN DISTINCT b</code> over 200,000 vertices and 1.8 million edges went from not finishing at depth 9 in 120 s to about half a second at depth 12 (<a href="https://github.com/ArcadeData/arcadedb/issues/10062">#10062</a>, contributed by <a href="https://github.com/tae898">@tae898</a>).</li>
  <li><strong>LSM index scans no longer go quadratic after a hot key</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9461">#9461</a>), bucket scan read-ahead is bounded by bytes (<a href="https://github.com/ArcadeData/arcadedb/issues/9404">#9404</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9401">#9401</a>), a commit skips the hole proof for pages written only by hole-free writes (<a href="https://github.com/ArcadeData/arcadedb/issues/9483">#9483</a>), and a scan stops paying a schema lookup per record (<a href="https://github.com/ArcadeData/arcadedb/issues/9997">#9997</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10013">#10013</a>).</li>
  <li><strong>Count push-downs through a Graph Analytical View</strong> (LSQB Q2, Q3, Q7, Q8, Q9) take property predicates, Cartesian products and pass-through <code class="language-plaintext highlighter-rouge">WITH</code>, and are exact on parallel, reciprocal, self-loop and light edges (<a href="https://github.com/ArcadeData/arcadedb/issues/9298">#9298</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9350">#9350</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9354">#9354</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9484">#9484</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9485">#9485</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9486">#9486</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9595">#9595</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9596">#9596</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9597">#9597</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9608">#9608</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9282">#9282</a>).</li>
  <li>An index lookup that overlaps a commit no longer re-reads the whole key up to 9 times (<a href="https://github.com/ArcadeData/arcadedb/issues/9902">#9902</a>).</li>
  <li><strong>Bolt</strong> coalesces RECORD messages into a few large socket writes (<a href="https://github.com/ArcadeData/arcadedb/pull/9353">#9353</a>) and no longer accumulates a whole PULL on the heap (<a href="https://github.com/ArcadeData/arcadedb/issues/9372">#9372</a>); a vector index rebuild is no longer deferred forever on a large index (<a href="https://github.com/ArcadeData/arcadedb/issues/7260">#7260</a>); the sparse top-K splits from 50,000 postings (<a href="https://github.com/ArcadeData/arcadedb/issues/9482">#9482</a>); and a time series grouped aggregation keeps its bucket cursor across rows (<a href="https://github.com/ArcadeData/arcadedb/issues/10003">#10003</a>).</li>
</ul>

<h2 id="security">Security</h2>

<p>Authorization, redaction and input-bounds fixes found by audit and by reporters.</p>

<ul>
  <li><strong>Long-lived wire connections re-check the user</strong> (<a href="https://github.com/ArcadeData/arcadedb/pull/9279">#9279</a>). PostgreSQL, Bolt, MongoDB and Redis sessions re-resolve their user against the live security state on every request, as HTTP does: a deleted user, a changed password or a revoked grant now applies to connections that are already open. <code class="language-plaintext highlighter-rouge">/ws</code> insert and <code class="language-plaintext highlighter-rouge">SUBSCRIBE</code> frames do the same (<a href="https://github.com/ArcadeData/arcadedb/issues/9311">#9311</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9312">#9312</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9340">#9340</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9424">#9424</a>).</li>
  <li><strong>SQL classification looks inside nested statements</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9628">#9628</a>). <code class="language-plaintext highlighter-rouge">LET $x = (INSERT ...)</code>, <code class="language-plaintext highlighter-rouge">SELECT FROM (CREATE TYPE ...)</code> and similar were reported as reads and ran through <code class="language-plaintext highlighter-rouge">GET</code>/<code class="language-plaintext highlighter-rouge">POST /api/v1/query</code> and read-only MCP identities. <code class="language-plaintext highlighter-rouge">PROFILE</code> follows the statement it wraps (<a href="https://github.com/ArcadeData/arcadedb/issues/9313">#9313</a>), and GraphQL type definitions need <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code> (<a href="https://github.com/ArcadeData/arcadedb/pull/9280">#9280</a>).</li>
  <li><strong>Lightweight edge types enforce <code class="language-plaintext highlighter-rouge">createRecord</code> and <code class="language-plaintext highlighter-rouge">deleteRecord</code></strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9619">#9619</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9779">#9779</a>); a vertex type’s super-node stripe buckets and a type’s paired <code class="language-plaintext highlighter-rouge">EXTERNAL</code> bucket follow the type’s permissions and lock (<a href="https://github.com/ArcadeData/arcadedb/issues/9780">#9780</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9637">#9637</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">DELETE_RECORD</code> is checked first</strong>, before any index or graph cleanup (<a href="https://github.com/ArcadeData/arcadedb/issues/9305">#9305</a>).</li>
  <li><strong>LOAD CSV settings are security-sensitive</strong> (<a href="https://github.com/ArcadeData/arcadedb/pull/9371">#9371</a>): remote URLs, local files, the import directory and the blocked IP ranges need <code class="language-plaintext highlighter-rouge">updateSecurity</code> to change through <code class="language-plaintext highlighter-rouge">ALTER DATABASE</code>.</li>
  <li><strong>Support diagnostics and logs redact <code class="language-plaintext highlighter-rouge">arcadedb.server.defaultDatabases</code></strong>, including the JVM arguments uploaded to the portal and the two log lines that spelled the setting with a space (<a href="https://github.com/ArcadeData/arcadedb/issues/9625">#9625</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9773">#9773</a>). <strong>Rotate any password configured this way on a server that already uploaded diagnostics.</strong> Studio’s Support page no longer disables a column’s auto-mask for good after one unmask (<a href="https://github.com/ArcadeData/arcadedb/issues/9797">#9797</a>).</li>
  <li><strong>Studio AI answers can no longer run HTML</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9626">#9626</a>): the answer and the profiler analysis use an escape-first markdown renderer, <code class="language-plaintext highlighter-rouge">marked</code> is no longer shipped, and Studio is served with a Content-Security-Policy (<a href="https://github.com/ArcadeData/arcadedb/issues/9681">#9681</a>). The AI Assistant caps every read of the portal and gateway answers (<a href="https://github.com/ArcadeData/arcadedb/issues/9627">#9627</a>).</li>
  <li><strong>gRPC</strong>: <code class="language-plaintext highlighter-rouge">arcadedb.grpc.tls.enabled</code> and the other <code class="language-plaintext highlighter-rouge">arcadedb.grpc.*</code> settings are read from <code class="language-plaintext highlighter-rouge">server-configuration.json</code> and the environment (asking for TLS in the file used to leave a plaintext endpoint listening), and <code class="language-plaintext highlighter-rouge">arcadedb.grpc.host</code> is honored (<a href="https://github.com/ArcadeData/arcadedb/issues/9316">#9316</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9318">#9318</a>).</li>
  <li><strong>Production mode conceals error text</strong> on the Gremlin Server for every channelizer, on the <code class="language-plaintext highlighter-rouge">/ws</code> insert session, Bolt and Redis (<a href="https://github.com/ArcadeData/arcadedb/issues/9317">#9317</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9639">#9639</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9749">#9749</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8749">#8749</a>). The Python server’s loopback-by-default hardening now covers the Postgres, Redis and Bolt listeners (<a href="https://github.com/ArcadeData/arcadedb/issues/9632">#9632</a>).</li>
  <li><strong>HA</strong>: security entries submitted from a node removed from the Raft configuration are refused (<a href="https://github.com/ArcadeData/arcadedb/issues/9589">#9589</a>).</li>
</ul>

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

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

<ul>
  <li>A schema reload keeps the page reservations, an interrupted flush resume no longer abandons batches, dropping a database with suspended flushes releases its WAL acknowledgements, and closing the last database inside <code class="language-plaintext highlighter-rouge">suspendFlushAndExecute</code> no longer fails with a <code class="language-plaintext highlighter-rouge">NullPointerException</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9835">#9835</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9832">#9832</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9881">#9881</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9927">#9927</a>).</li>
  <li>A subtype that inherits an <code class="language-plaintext highlighter-rouge">EXTERNAL</code> property stores its values in its own <code class="language-plaintext highlighter-rouge">_ext</code> bucket (<a href="https://github.com/ArcadeData/arcadedb/issues/9683">#9683</a>). A compacted LSM bucket index keeps its name across a restart and an HA schema reload (<a href="https://github.com/ArcadeData/arcadedb/issues/9213">#9213</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9434">#9434</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">CHECK DATABASE</code> reads each record of a type with <code class="language-plaintext highlighter-rouge">MANDATORY</code> / <code class="language-plaintext highlighter-rouge">NOTNULL</code> properties once (<a href="https://github.com/ArcadeData/arcadedb/issues/7981">#7981</a>), and a failed read of a closed component file throws <code class="language-plaintext highlighter-rouge">IllegalArgumentException</code> instead of <code class="language-plaintext highlighter-rouge">NullPointerException</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9306">#9306</a>).</li>
  <li>Interrupted snapshot reads reopen the channel instead of invalidating open windows or aborting a backup (<a href="https://github.com/ArcadeData/arcadedb/issues/9306">#9306</a>); a full backup skips index-compaction temporaries (<a href="https://github.com/ArcadeData/arcadedb/issues/8848">#8848</a>); orphan compaction temporaries are swept at open (<a href="https://github.com/ArcadeData/arcadedb/issues/8849">#8849</a>).</li>
  <li>An impossible <code class="language-plaintext highlighter-rouge">REGEXP</code>, and a <code class="language-plaintext highlighter-rouge">MANDATORY</code> or default-valued property on a <code class="language-plaintext highlighter-rouge">LIGHTWEIGHT</code> edge type, are refused at declaration time (<a href="https://github.com/ArcadeData/arcadedb/issues/9853">#9853</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9846">#9846</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9990">#9990</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9975">#9975</a>).</li>
</ul>

<h3 id="sql-and-indexes">SQL and Indexes</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">LIKE '%'</code> matches an empty string (<a href="https://github.com/ArcadeData/arcadedb/issues/10005">#10005</a>), <code class="language-plaintext highlighter-rouge">values()</code> on a sub-query row lists exactly the values of <code class="language-plaintext highlighter-rouge">keys()</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9843">#9843</a>), nested projection prefix wildcards <code class="language-plaintext highlighter-rouge">@this:{s*}</code> work (<a href="https://github.com/ArcadeData/arcadedb/issues/9841">#9841</a>), and a bare <code class="language-plaintext highlighter-rouge">ORDER BY @rid</code> on a <code class="language-plaintext highlighter-rouge">MATCH</code> that returns columns is refused instead of silently ignored (<a href="https://github.com/ArcadeData/arcadedb/issues/9911">#9911</a>).</li>
  <li>A <code class="language-plaintext highlighter-rouge">MATCH</code> node with <code class="language-plaintext highlighter-rouge">rid:</code> honors its <code class="language-plaintext highlighter-rouge">type:</code> and <code class="language-plaintext highlighter-rouge">bucket:</code>, and a node filtered by <code class="language-plaintext highlighter-rouge">SEARCH_INDEX()</code> is streamed instead of loaded whole (<a href="https://github.com/ArcadeData/arcadedb/issues/9912">#9912</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9908">#9908</a>). <code class="language-plaintext highlighter-rouge">SEARCH_INDEX</code> pushes <code class="language-plaintext highlighter-rouge">@rid</code> ranges and merges score-ordered <code class="language-plaintext highlighter-rouge">OR</code> branches before loading records (<a href="https://github.com/ArcadeData/arcadedb/issues/9895">#9895</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9909">#9909</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10022">#10022</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">UPDATE ... UPSERT</code> requires an equality on every property of a UNIQUE index (<a href="https://github.com/ArcadeData/arcadedb/issues/9339">#9339</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9342">#9342</a>); an async <code class="language-plaintext highlighter-rouge">SELECT</code> with parameters binds them, not the executor’s configuration (<a href="https://github.com/ArcadeData/arcadedb/issues/9941">#9941</a>); concurrent <code class="language-plaintext highlighter-rouge">CREATE INDEX IF NOT EXISTS</code> no longer fails with a <code class="language-plaintext highlighter-rouge">NullPointerException</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9741">#9741</a>); <code class="language-plaintext highlighter-rouge">Schema.copyType</code> no longer rolls back the caller’s transaction (<a href="https://github.com/ArcadeData/arcadedb/issues/9851">#9851</a>).</li>
  <li>The BM25 corpus counters of a <code class="language-plaintext highlighter-rouge">FULL_TEXT</code> index move when the transaction commits on every node and no longer race writers; the repair is kept and the counters are inspectable (<a href="https://github.com/ArcadeData/arcadedb/issues/10018">#10018</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10019">#10019</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9337">#9337</a>). <code class="language-plaintext highlighter-rouge">SEARCH_FIELDS</code> no longer matches terms in fields it was not given (<a href="https://github.com/ArcadeData/arcadedb/issues/9910">#9910</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">vector.sum</code>, <code class="language-plaintext highlighter-rouge">vector.avg</code>, <code class="language-plaintext highlighter-rouge">vector.min</code> and <code class="language-plaintext highlighter-rouge">vector.max</code> aggregate when written with the dotted name, in SQL and openCypher (<a href="https://github.com/ArcadeData/arcadedb/issues/9768">#9768</a>); <code class="language-plaintext highlighter-rouge">sum()</code> and <code class="language-plaintext highlighter-rouge">avg()</code> work over <code class="language-plaintext highlighter-rouge">BYTE</code> values, and <code class="language-plaintext highlighter-rouge">agg.*</code> functions accept <code class="language-plaintext highlighter-rouge">Float[]</code> and <code class="language-plaintext highlighter-rouge">Short[]</code> properties (<a href="https://github.com/ArcadeData/arcadedb/issues/9281">#9281</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9777">#9777</a>).</li>
  <li>A <code class="language-plaintext highlighter-rouge">COLLATE ci</code> index is skipped by native <code class="language-plaintext highlighter-rouge">select()</code> range cursors and Gremlin range <code class="language-plaintext highlighter-rouge">has()</code>, and <code class="language-plaintext highlighter-rouge">select()</code> with a <code class="language-plaintext highlighter-rouge">LIMIT</code> returns the rows the unindexed query does (<a href="https://github.com/ArcadeData/arcadedb/issues/9302">#9302</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9613">#9613</a>).</li>
</ul>

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

<ul>
  <li><strong>GQL constructs the parser accepted and the engine ignored are refused</strong> (<code class="language-plaintext highlighter-rouge">ANY SHORTEST</code>, <code class="language-plaintext highlighter-rouge">REPEATABLE ELEMENTS</code>, <code class="language-plaintext highlighter-rouge">IS DIRECTED</code>, a nested quantified group), a path variable over a quantified group binds one <code class="language-plaintext highlighter-rouge">PATH</code>, <code class="language-plaintext highlighter-rouge">LET</code>, <code class="language-plaintext highlighter-rouge">IS NORMALIZED</code> and the conditional query <code class="language-plaintext highlighter-rouge">WHEN ... THEN</code> run, and <code class="language-plaintext highlighter-rouge">RETURN ... ORDER BY/SKIP/LIMIT</code> is applied after the sort as one page (<a href="https://github.com/ArcadeData/arcadedb/issues/9940">#9940</a>). The full picture of what runs today is in <a href="https://arcadedb.com/blog/iso-gql-in-arcadedb/">ISO GQL in ArcadeDB: What Works Today</a>.</li>
  <li>A variable-length <code class="language-plaintext highlighter-rouge">MATCH</code> honors a previously bound relationship list (<a href="https://github.com/ArcadeData/arcadedb/issues/9828">#9828</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9854">#9854</a>, contributed by <a href="https://github.com/mvanhorn">@mvanhorn</a>), <code class="language-plaintext highlighter-rouge">WHERE (booleanVariable)</code> is no longer parsed as a pattern (<a href="https://github.com/ArcadeData/arcadedb/issues/9542">#9542</a>), and a variable cannot be both a node and a relationship in a pattern predicate (<a href="https://github.com/ArcadeData/arcadedb/issues/9292">#9292</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">DROP CONSTRAINT &lt;name&gt;</code> drops what the constraint created, <code class="language-plaintext highlighter-rouge">SHOW CONSTRAINTS</code> / <code class="language-plaintext highlighter-rouge">SHOW INDEXES</code> list what exists, and <code class="language-plaintext highlighter-rouge">IS :: &lt;type&gt;</code> keeps the property’s other constraints and refuses attributes the new type cannot use (<a href="https://github.com/ArcadeData/arcadedb/issues/9646">#9646</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9706">#9706</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9304">#9304</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9645">#9645</a>).</li>
  <li>Write conflicts are retried by <code class="language-plaintext highlighter-rouge">Database.transaction(..., retries)</code> like SQL’s (<a href="https://github.com/ArcadeData/arcadedb/issues/9487">#9487</a>); temporal values compare under every <code class="language-plaintext highlighter-rouge">dateImplementation</code> and render with <code class="language-plaintext highlighter-rouge">Locale.ROOT</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9647">#9647</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9649">#9649</a>); durations are exact, ISO text is read as Neo4j reads it, and arithmetic raises on overflow (<a href="https://github.com/ArcadeData/arcadedb/issues/9338">#9338</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10055">#10055</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">algo.bridges</code> treats edges as undirected and <code class="language-plaintext highlighter-rouge">algo.kShortestPaths</code> takes a direction (<a href="https://github.com/ArcadeData/arcadedb/issues/9766">#9766</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9914">#9914</a>), and <code class="language-plaintext highlighter-rouge">algo.*</code> keeps <code class="language-plaintext highlighter-rouge">tolerance</code> and <code class="language-plaintext highlighter-rouge">direction</code> on the accelerated path (<a href="https://github.com/ArcadeData/arcadedb/issues/9829">#9829</a>).</li>
</ul>

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

<ul>
  <li>A view over only some of the vertex types no longer accelerates count push-downs, traversal, shortest paths or <code class="language-plaintext highlighter-rouge">algo.*</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9301">#9301</a>); a lightweight edge created or removed after the build reaches the view (<a href="https://github.com/ArcadeData/arcadedb/issues/9572">#9572</a>); a vertex whose edge list has an empty segment between non-empty ones no longer loses edges in the view or in <code class="language-plaintext highlighter-rouge">CHECK DATABASE</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9715">#9715</a>); parallel lightweight edges are distinct relationships in <code class="language-plaintext highlighter-rouge">=</code>, <code class="language-plaintext highlighter-rouge">DISTINCT</code> and <code class="language-plaintext highlighter-rouge">DELETE</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/10008">#10008</a>).</li>
  <li>A view restored from its persisted CSR after a reopen gives the speed-up it reports (<a href="https://github.com/ArcadeData/arcadedb/issues/9587">#9587</a>), a fused 2-hop <code class="language-plaintext highlighter-rouge">count(*)</code> no longer holds every path on the heap (<a href="https://github.com/ArcadeData/arcadedb/issues/9738">#9738</a>), and a plain <code class="language-plaintext highlighter-rouge">OPTIONAL MATCH ... WHERE h IS NULL</code> plans as an anti-join (<a href="https://github.com/ArcadeData/arcadedb/issues/9598">#9598</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">GraphBatch.withLightEdges(true)</code> needs a <code class="language-plaintext highlighter-rouge">LIGHTWEIGHT</code> edge type (<a href="https://github.com/ArcadeData/arcadedb/issues/9378">#9378</a>).</li>
</ul>

<h3 id="vector-and-sparse-vector">Vector and Sparse Vector</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">vectorNeighbors()</code>, <code class="language-plaintext highlighter-rouge">vector.neighbors</code>, <code class="language-plaintext highlighter-rouge">db.index.vector.queryNodes</code>, <code class="language-plaintext highlighter-rouge">vector.sparseNeighbors</code> and <code class="language-plaintext highlighter-rouge">vector.recommend</code> (grouped or not) top up candidates that a concurrent commit deleted instead of answering short (<a href="https://github.com/ArcadeData/arcadedb/issues/9368">#9368</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9858">#9858</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9697">#9697</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10002">#10002</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9692">#9692</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9396">#9396</a>); the <code class="language-plaintext highlighter-rouge">db.index.vector.queryNodes</code> report is from <a href="https://github.com/ofir-blaus">@ofir-blaus</a>.</li>
  <li><code class="language-plaintext highlighter-rouge">BINARY</code> quantization reranks past deleted candidates and in grouped searches (<a href="https://github.com/ArcadeData/arcadedb/issues/9633">#9633</a>), <code class="language-plaintext highlighter-rouge">vector.approxDistance</code> <code class="language-plaintext highlighter-rouge">INT8</code> keeps its per-vector scale (<a href="https://github.com/ArcadeData/arcadedb/issues/9776">#9776</a>), a sparse-vector update answers on its new dimensions (<a href="https://github.com/ArcadeData/arcadedb/issues/9343">#9343</a>), and FP16 and INT8 sparse searches no longer miss a document that belongs in the top-K (<a href="https://github.com/ArcadeData/arcadedb/issues/9834">#9834</a>).</li>
  <li>A location load that stops partway no longer lets new vectors reuse ids held by other records (<a href="https://github.com/ArcadeData/arcadedb/issues/9892">#9892</a>).</li>
</ul>

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

<ul>
  <li><strong>Postgres</strong>: <code class="language-plaintext highlighter-rouge">SHOW</code>, <code class="language-plaintext highlighter-rouge">DISCARD</code>, <code class="language-plaintext highlighter-rouge">DEALLOCATE</code>, <code class="language-plaintext highlighter-rouge">CLOSE</code> and <code class="language-plaintext highlighter-rouge">search_path</code> are handled; <code class="language-plaintext highlighter-rouge">START TRANSACTION</code>, <code class="language-plaintext highlighter-rouge">ABORT</code>, <code class="language-plaintext highlighter-rouge">BEGIN</code> with isolation modes and <code class="language-plaintext highlighter-rouge">AND NO CHAIN</code> are accepted; <code class="language-plaintext highlighter-rouge">statement_timeout</code> is enforced; the extended protocol forwards a follower’s DDL and writes to the leader; and a write inside an explicit transaction on a follower stays inside it (<a href="https://github.com/ArcadeData/arcadedb/issues/9326">#9326</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9327">#9327</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9328">#9328</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9329">#9329</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9631">#9631</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9954">#9954</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10040">#10040</a>).</li>
  <li><strong>Bolt</strong>: an index compaction left half-replicated no longer breaks every lookup with <code class="language-plaintext highlighter-rouge">ArrayIndexOutOfBoundsException</code> on a follower (<a href="https://github.com/ArcadeData/arcadedb/issues/10046">#10046</a>, reported by <a href="https://github.com/rlaveycal">@rlaveycal</a>).</li>
  <li><strong>MongoDB</strong>: a <code class="language-plaintext highlighter-rouge">MAP</code> property with a non-String key no longer fails the whole <code class="language-plaintext highlighter-rouge">find()</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9973">#9973</a>), and a follower’s refusal is <code class="language-plaintext highlighter-rouge">NotWritablePrimary</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9845">#9845</a>).</li>
  <li><strong>Redis</strong>: a <code class="language-plaintext highlighter-rouge">MULTI</code>/<code class="language-plaintext highlighter-rouge">EXEC</code> block retried by the HTTP auto-commit wrapper no longer applies <code class="language-plaintext highlighter-rouge">INCR</code> twice, <code class="language-plaintext highlighter-rouge">HDEL</code> reports the records it removed and a multi-key <code class="language-plaintext highlighter-rouge">HDEL</code> / <code class="language-plaintext highlighter-rouge">HSET</code> is one unit of work (<a href="https://github.com/ArcadeData/arcadedb/issues/9322">#9322</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9837">#9837</a>).</li>
  <li><strong>gRPC</strong>: <code class="language-plaintext highlighter-rouge">ExecuteCommand</code> honors <code class="language-plaintext highlighter-rouge">grpcQueryMaxResultRows</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9861">#9861</a>), <code class="language-plaintext highlighter-rouge">arcadedb.grpc.compression.enabled</code> is applied (<a href="https://github.com/ArcadeData/arcadedb/issues/9782">#9782</a>), a lost answer on a write is an unknown outcome, not a retryable error (<a href="https://github.com/ArcadeData/arcadedb/issues/8822">#8822</a>), and <code class="language-plaintext highlighter-rouge">ZONED_DATETIME</code>, <code class="language-plaintext highlighter-rouge">OFFSET_TIME</code>, <code class="language-plaintext highlighter-rouge">LOCAL_TIME</code> and <code class="language-plaintext highlighter-rouge">DURATION</code> keep their type on the wire (<a href="https://github.com/ArcadeData/arcadedb/issues/9969">#9969</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10034">#10034</a>).</li>
  <li><strong>GraphQL</strong>: selections that share a response key are merged, and a sub-selection on a <code class="language-plaintext highlighter-rouge">LINK</code> property is no longer dropped (<a href="https://github.com/ArcadeData/arcadedb/issues/8744">#8744</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9467">#9467</a>).</li>
  <li><strong>Gremlin</strong>: <code class="language-plaintext highlighter-rouge">elementMap()</code> rows have one JSON shape, a nested map is named like the row (<code class="language-plaintext highlighter-rouge">@rid</code>, <code class="language-plaintext highlighter-rouge">@type</code>), the plugin fails its start on a port bind error, and the index step is guarded against a concurrent <code class="language-plaintext highlighter-rouge">CREATE INDEX</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9576">#9576</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9781">#9781</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9319">#9319</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9651">#9651</a>).</li>
  <li><strong>HTTP</strong>: a statement longer than <code class="language-plaintext highlighter-rouge">arcadedb.network.socketTimeout</code> is no longer killed with no response (<a href="https://github.com/ArcadeData/arcadedb/issues/9953">#9953</a>), the write watchdog survives an XNIO selector wakeup race (<a href="https://github.com/ArcadeData/arcadedb/issues/9439">#9439</a>), and the streamed query error line carries the classified label and <code class="language-plaintext highlighter-rouge">retryAfter</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/8899">#8899</a>).</li>
  <li><strong>Remote client</strong>: <code class="language-plaintext highlighter-rouge">SELECT @rid FROM Person</code> and Gremlin <code class="language-plaintext highlighter-rouge">elementMap()</code> keep their metadata columns (<a href="https://github.com/ArcadeData/arcadedb/issues/9804">#9804</a>), and a malformed <code class="language-plaintext highlighter-rouge">DuplicatedKeyException</code> no longer crashes the driver (<a href="https://github.com/ArcadeData/arcadedb/issues/9473">#9473</a>).</li>
</ul>

<h3 id="importer-graphbatch-and-async">Importer, GraphBatch and Async</h3>

<ul>
  <li>A 200M-vertex / 2B-edge <code class="language-plaintext highlighter-rouge">GraphImporter</code> load no longer stalls in pass 1 (<a href="https://github.com/ArcadeData/arcadedb/issues/9575">#9575</a>); an async executor batch waits a jittered backoff before replay (<a href="https://github.com/ArcadeData/arcadedb/issues/9529">#9529</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9694">#9694</a>).</li>
  <li>The native image resolves reflection-free SQL AST copies, Lucene and the missing function metadata, which fixed <code class="language-plaintext highlighter-rouge">UPDATE</code>/<code class="language-plaintext highlighter-rouge">DELETE</code> over HTTP answering 500 with <code class="language-plaintext highlighter-rouge">NoSuchMethodException</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9495">#9495</a>, reported by <a href="https://github.com/zek0">@zek0</a>), and registers the gRPC messages and Netty’s ticker (<a href="https://github.com/ArcadeData/arcadedb/pull/10070">#10070</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9564">#9564</a>).</li>
</ul>

<h3 id="python-bindings-and-tooling">Python Bindings and Tooling</h3>

<ul>
  <li>The Python bindings are updated for 26.10.1: results that release the engine, exact <code class="language-plaintext highlighter-rouge">Decimal</code> and <code class="language-plaintext highlighter-rouge">bytes</code> parameters, and tripwires for open engine bugs (<a href="https://github.com/ArcadeData/arcadedb/pull/9294">#9294</a>, contributed by <a href="https://github.com/tae898">@tae898</a>). An interrupt during <code class="language-plaintext highlighter-rouge">begin()</code> pops only the level it pushed (<a href="https://github.com/ArcadeData/arcadedb/issues/9970">#9970</a>).</li>
</ul>

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

<p>Behavior changes are collected in the <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.10.2">release notes</a>, and the complete write-up, including the temporals migration recipe, is in <a href="https://github.com/ArcadeData/arcadedb/blob/26.10.2/docs/release-26.10.2.md"><code class="language-plaintext highlighter-rouge">docs/release-26.10.2.md</code></a>. The ones worth checking before you upgrade:</p>

<ol>
  <li><strong>The query admission gate is on by default</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/9518">#9518</a>). Past <code class="language-plaintext highlighter-rouge">arcadedb.queryQueueTimeout</code> or <code class="language-plaintext highlighter-rouge">arcadedb.queryQueueMaxSize</code> a request is refused as retryable (HTTP 503, Postgres <code class="language-plaintext highlighter-rouge">40001</code>, Redis <code class="language-plaintext highlighter-rouge">TRYAGAIN</code>). Set <code class="language-plaintext highlighter-rouge">arcadedb.queryMaxConcurrent=0</code> to disable it.</li>
  <li><strong>Docker images run on Java 25, and Studio is served with a Content-Security-Policy</strong> that refuses inline script (<a href="https://github.com/ArcadeData/arcadedb/issues/9602">#9602</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9681">#9681</a>).</li>
  <li><strong>HA</strong>:
    <ul>
      <li>The snapshot ship compresses at DEFLATE level 1 by default, so a resync over a WAN sends more bytes; <code class="language-plaintext highlighter-rouge">arcadedb.ha.snapshotCompressionLevel=6</code> restores the previous level (<a href="https://github.com/ArcadeData/arcadedb/pull/9295">#9295</a>).</li>
      <li>A follower restarted in place is no longer reformatted automatically (<a href="https://github.com/ArcadeData/arcadedb/issues/8901">#8901</a>).</li>
      <li>During a rolling upgrade a type rename, <code class="language-plaintext highlighter-rouge">ALTER DATABASE</code> and <code class="language-plaintext highlighter-rouge">set database setting</code> are refused with 409 until every node runs this build, and a rename cannot be downgraded past this release on its retained log (<a href="https://github.com/ArcadeData/arcadedb/issues/9762">#9762</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9763">#9763</a>).</li>
      <li>An index that already lost vectors to <a href="https://github.com/ArcadeData/arcadedb/issues/9428">#9428</a> stays short and has to be rebuilt.</li>
    </ul>
  </li>
  <li><strong>A follower’s refusal is a redirect</strong>: Bolt <code class="language-plaintext highlighter-rouge">Neo.ClientError.Cluster.NotALeader</code>, MongoDB <code class="language-plaintext highlighter-rouge">10107 NotWritablePrimary</code>, Redis <code class="language-plaintext highlighter-rouge">READONLY</code>, <code class="language-plaintext highlighter-rouge">/ws</code> and gRPC <code class="language-plaintext highlighter-rouge">NOT_LEADER</code> with the leader’s address. A client that matched the old retryable codes must handle the new ones (<a href="https://github.com/ArcadeData/arcadedb/issues/9845">#9845</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9868">#9868</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9930">#9930</a>).</li>
  <li><strong>Queries that answered differently now answer consistently</strong>:
    <ul>
      <li>a plain <code class="language-plaintext highlighter-rouge">=</code> / <code class="language-plaintext highlighter-rouge">IN</code> on a <code class="language-plaintext highlighter-rouge">COLLATE ci</code> index is case sensitive (<a href="https://github.com/ArcadeData/arcadedb/issues/9403">#9403</a>);</li>
      <li><code class="language-plaintext highlighter-rouge">k = &lt;list&gt;</code> no longer expands (<a href="https://github.com/ArcadeData/arcadedb/issues/9370">#9370</a>), SQL <code class="language-plaintext highlighter-rouge">sum()</code> of an empty or all-NULL group is NULL (<a href="https://github.com/ArcadeData/arcadedb/issues/9351">#9351</a>), and <code class="language-plaintext highlighter-rouge">UPSERT</code> needs a UNIQUE index (<a href="https://github.com/ArcadeData/arcadedb/issues/9339">#9339</a>);</li>
      <li><code class="language-plaintext highlighter-rouge">dijkstra()</code>, <code class="language-plaintext highlighter-rouge">astar()</code> and the <code class="language-plaintext highlighter-rouge">algo.*</code> finders count a missing weight as 1 and skip negative weights (<a href="https://github.com/ArcadeData/arcadedb/issues/9443">#9443</a>);</li>
      <li><code class="language-plaintext highlighter-rouge">MERGE</code> and <code class="language-plaintext highlighter-rouge">MATCH</code> agree, and a declared scalar property matches the text it stored (<a href="https://github.com/ArcadeData/arcadedb/issues/9693">#9693</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9695">#9695</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9794">#9794</a>);</li>
      <li><code class="language-plaintext highlighter-rouge">path.expandconfig</code> and <code class="language-plaintext highlighter-rouge">path.expand</code> return every trail by default, and <code class="language-plaintext highlighter-rouge">path.subgraphall</code> returns every relationship between its nodes (<a href="https://github.com/ArcadeData/arcadedb/issues/10025">#10025</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10048">#10048</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10049">#10049</a>);</li>
      <li><code class="language-plaintext highlighter-rouge">include()</code> and <code class="language-plaintext highlighter-rouge">exclude()</code> return a map (<a href="https://github.com/ArcadeData/arcadedb/issues/9840">#9840</a>), <code class="language-plaintext highlighter-rouge">values()</code> on a <code class="language-plaintext highlighter-rouge">Result</code> follows <code class="language-plaintext highlighter-rouge">keys()</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9843">#9843</a>), a bare <code class="language-plaintext highlighter-rouge">ORDER BY @rid</code> on a <code class="language-plaintext highlighter-rouge">MATCH</code> with columns is refused (<a href="https://github.com/ArcadeData/arcadedb/issues/9911">#9911</a>), and a path variable over a quantified group binds one <code class="language-plaintext highlighter-rouge">PATH</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9940">#9940</a>).</li>
    </ul>
  </li>
  <li><strong>openCypher temporals are stored natively.</strong> A database that holds Cypher temporals in undeclared properties reads them back as Strings until the property is declared with the new type and the values are copied through a parked property (<a href="https://github.com/ArcadeData/arcadedb/issues/8572">#8572</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9636">#9636</a>).</li>
  <li><strong>Stricter refusals instead of silent acceptance</strong>:
    <ul>
      <li>time series writes with an empty tag value or an undeclared key; <code class="language-plaintext highlighter-rouge">arcadedb.timeSeriesUndeclaredKeys=ignore</code> restores the old behavior (<a href="https://github.com/ArcadeData/arcadedb/issues/8646">#8646</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8647">#8647</a>);</li>
      <li>an ISO duration with no component or a misplaced fraction (<a href="https://github.com/ArcadeData/arcadedb/issues/10055">#10055</a>);</li>
      <li>an impossible <code class="language-plaintext highlighter-rouge">REGEXP</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9853">#9853</a>);</li>
      <li>a <code class="language-plaintext highlighter-rouge">MANDATORY</code> or default-valued property on a <code class="language-plaintext highlighter-rouge">LIGHTWEIGHT</code> edge type (<a href="https://github.com/ArcadeData/arcadedb/issues/9846">#9846</a>);</li>
      <li>a statement that hides a write inside a read (<a href="https://github.com/ArcadeData/arcadedb/issues/9628">#9628</a>);</li>
      <li><code class="language-plaintext highlighter-rouge">GraphBatch.withLightEdges(true)</code> into a type that is not <code class="language-plaintext highlighter-rouge">LIGHTWEIGHT</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/9378">#9378</a>);</li>
      <li>the GQL constructs listed above (<a href="https://github.com/ArcadeData/arcadedb/issues/9940">#9940</a>).</li>
    </ul>
  </li>
  <li><strong>PromQL refuses what Prometheus refuses</strong>: a comparison between two scalars without <code class="language-plaintext highlighter-rouge">bool</code>, a set operator with a scalar, a range vector or a string as an operand, a scalar argument to <code class="language-plaintext highlighter-rouge">abs</code>, <code class="language-plaintext highlighter-rouge">ceil</code>, <code class="language-plaintext highlighter-rouge">floor</code> and <code class="language-plaintext highlighter-rouge">round</code>, and a range query that returns a range vector or a string (<a href="https://github.com/ArcadeData/arcadedb/issues/9723">#9723</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9743">#9743</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9788">#9788</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9883">#9883</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9922">#9922</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9924">#9924</a>).</li>
  <li><strong>Existing names answer 409</strong>: creating a user, a token or a database under a taken name no longer answers 403 or 400 (<a href="https://github.com/ArcadeData/arcadedb/issues/10073">#10073</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10074">#10074</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/10075">#10075</a>).</li>
  <li><strong>Vector graphs</strong>: a snapshot install and a full backup now ship the <code class="language-plaintext highlighter-rouge">.vecgraph</code> empty, so the first search after a restore builds and verifies its own graph. Run <code class="language-plaintext highlighter-rouge">REBUILD INDEX</code> on the vector indexes of archives taken before this release (<a href="https://github.com/ArcadeData/arcadedb/issues/9758">#9758</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9888">#9888</a>).</li>
  <li><strong>Subtype <code class="language-plaintext highlighter-rouge">EXTERNAL</code> values</strong> written before the upgrade stay inline until <code class="language-plaintext highlighter-rouge">REBUILD TYPE &lt;Subtype&gt;</code> moves them (<a href="https://github.com/ArcadeData/arcadedb/issues/9683">#9683</a>).</li>
  <li><strong>Rotate passwords set through <code class="language-plaintext highlighter-rouge">arcadedb.server.defaultDatabases</code></strong> if the server already uploaded diagnostics (<a href="https://github.com/ArcadeData/arcadedb/issues/9625">#9625</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9773">#9773</a>).</li>
  <li><strong>Smaller changes</strong>: the Gremlin Server in production mode refuses to start with a channelizer that cannot hide error details (<a href="https://github.com/ArcadeData/arcadedb/issues/9639">#9639</a>); <code class="language-plaintext highlighter-rouge">BACKUP DATABASE</code> now prunes the archive directory by the retention policy (<a href="https://github.com/ArcadeData/arcadedb/issues/9844">#9844</a>); <code class="language-plaintext highlighter-rouge">arcadedb.grpc.compression.enabled=false</code> is honored (<a href="https://github.com/ArcadeData/arcadedb/issues/9782">#9782</a>).</li>
  <li><strong>Embedded API</strong>: <code class="language-plaintext highlighter-rouge">SQLFunctionHeuristicPathFinderAbstract.MIN</code> is gone (use <code class="language-plaintext highlighter-rouge">com.arcadedb.graph.EdgeWeight</code>), <code class="language-plaintext highlighter-rouge">Schema.alterMaterializedView()</code> validates its arguments, and schema setters called directly on an HA follower throw the “schema changes need the leader” error (<a href="https://github.com/ArcadeData/arcadedb/issues/9443">#9443</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8726">#8726</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9747">#9747</a>). If you use ArcadeDB <a href="https://arcadedb.com/embedded.html">embedded</a>, check these before you bump the dependency.</li>
</ol>

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

<p>About 30 dependency bumps landed in this cycle, almost all through Dependabot. The notable ones:</p>

<ul>
  <li><strong>Server and engine</strong>: Netty <strong>4.2.19</strong>, Undertow <strong>2.4.4</strong>, Lucene <strong>10.5.2</strong>, Jedis <strong>8.0.2</strong>, JLine <strong>4.4.7</strong>, snappy-java <strong>1.1.10.11</strong> and the PostgreSQL JDBC driver <strong>42.7.14</strong>. The Docker base image moves to Eclipse Temurin 25.</li>
  <li><strong>Studio</strong>: ApexCharts <strong>7.8.0</strong>, swagger-ui-dist <strong>5.33.1</strong> and datatables.net <strong>3.1.3</strong>; <code class="language-plaintext highlighter-rouge">marked</code> is no longer shipped (<a href="https://github.com/ArcadeData/arcadedb/issues/9626">#9626</a>).</li>
  <li><strong>Build and test</strong>: Cucumber <strong>8.0.4</strong>, Playwright <strong>1.64</strong>, black <strong>26.10.0</strong> and the Go module bumps of the e2e suite.</li>
  <li><strong>CI</strong>: GitHub Actions and pre-commit hook bumps.</li>
</ul>

<h2 id="getting-started-with-26102">Getting Started with 26.10.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.10.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.10.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 details on features and usage, see the <a href="https://docs.arcadedb.com/">documentation</a>.</p>

<hr />

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

<p>Thanks to everyone who reported, reproduced, reviewed, tested and fixed, in particular <a href="https://github.com/ruispereira">@ruispereira</a>, <a href="https://github.com/tae898">@tae898</a>, <a href="https://github.com/manus-pi">@manus-pi</a>, <a href="https://github.com/ofir-blaus">@ofir-blaus</a>, <a href="https://github.com/blaus23">@blaus23</a>, <a href="https://github.com/mvanhorn">@mvanhorn</a>, <a href="https://github.com/willemijn-zeno">@willemijn-zeno</a>, <a href="https://github.com/shulei5831sl">@shulei5831sl</a>, <a href="https://github.com/YaeSakuraQ">@YaeSakuraQ</a>, <a href="https://github.com/Das-Rabindra">@Das-Rabindra</a>, <a href="https://github.com/HarinathMaddela">@HarinathMaddela</a>, <a href="https://github.com/saeedtabrizi">@saeedtabrizi</a>, <a href="https://github.com/zpaulo80">@zpaulo80</a>, <a href="https://github.com/zek0">@zek0</a>, <a href="https://github.com/YGY-001">@YGY-001</a>, <a href="https://github.com/ruslan-butyk-fntext">@ruslan-butyk-fntext</a>, <a href="https://github.com/rlaveycal">@rlaveycal</a>, <a href="https://github.com/preshewar">@preshewar</a> and <a href="https://github.com/philiptran-tech">@philiptran-tech</a>.</p>

<p>Luca Garulli
ArcadeDB Founder</p>]]></content><author><name>Luca Garulli</name></author><category term="Multi-Model" /><category term="High Availability" /><category term="Raft" /><category term="Time Series" /><category term="OpenCypher" /><category term="Security" /><category term="Graph Database" /><category term="Release" /><summary type="html"><![CDATA[ArcadeDB 26.10.2 closes 682 issues and pull requests across about 1,360 commits. A query admission gate and list/terminate for running queries arrive, point lookups get 3 to 7 times faster through a shape-based plan cache, openCypher temporals are stored natively, Raft HA replicates every schema change, and the time series sealed store stops losing data.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/release-v26.10.2.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/release-v26.10.2.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ISO GQL in ArcadeDB: What Works Today</title><link href="https://arcadedb.com/blog/iso-gql-in-arcadedb/" rel="alternate" type="text/html" title="ISO GQL in ArcadeDB: What Works Today" /><published>2026-10-13T00:00:00+00:00</published><updated>2026-10-13T00:00:00+00:00</updated><id>https://arcadedb.com/blog/iso-gql-in-arcadedb</id><content type="html" xml:base="https://arcadedb.com/blog/iso-gql-in-arcadedb/"><![CDATA[<p>GQL is the first new ISO database query language since SQL. The standard, ISO/IEC 39075, was published in April 2024, and graph databases have been moving toward it since. We have just published a <a href="https://docs.arcadedb.com/arcadedb/reference/cypher/cypher-gql-conformance">GQL coverage statement</a> for ArcadeDB that lists, feature by feature, what is supported, what is partial, and what is missing.</p>

<p>This post is the short version: how GQL fits into ArcadeDB, what you can use today, and where the gaps are.</p>

<h2 id="how-gql-runs-in-arcadedb">How GQL runs in ArcadeDB</h2>

<p>ArcadeDB has no separate GQL engine. GQL syntax runs inside the same native engine that implements OpenCypher 25, the latest Cypher grammar. That has two practical consequences:</p>

<ul>
  <li><strong>Nothing to switch on.</strong> You send GQL to the same Cypher endpoint you already use, over HTTP, the Bolt protocol or the Java API.</li>
  <li><strong>GQL and Cypher mix freely.</strong> A GQL clause and a Cypher clause can sit in the same query, so you can adopt GQL one clause at a time.</li>
</ul>

<h2 id="what-works-today">What works today</h2>

<h3 id="graph-pattern-matching-with-gql-quantifiers">Graph pattern matching with GQL quantifiers</h3>

<p>Node and edge patterns, label expressions, <code class="language-plaintext highlighter-rouge">OPTIONAL MATCH</code> and GQL quantified edges all work:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">start:</span><span class="n">Station</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'A'</span><span class="ss">})</span><span class="o">-</span><span class="ss">[</span><span class="nc">:NEXT</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">{</span><span class="mi">1</span><span class="ss">,</span><span class="mi">2</span><span class="ss">}(</span><span class="py">stop:</span><span class="n">Station</span><span class="ss">)</span>
<span class="k">RETURN</span> <span class="n">stop.name</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-&gt;{1,2}</code> is the GQL way to say “one or two hops”, the counterpart of Cypher’s <code class="language-plaintext highlighter-rouge">*1..2</code>.</p>

<h3 id="path-modes-trail-walk-and-acyclic">Path modes: TRAIL, WALK and ACYCLIC</h3>

<p>GQL path modes control whether a variable-length match may revisit edges or nodes. <code class="language-plaintext highlighter-rouge">TRAIL</code> (no repeated edges) is the default; <code class="language-plaintext highlighter-rouge">ACYCLIC</code> forbids repeated nodes; <code class="language-plaintext highlighter-rouge">WALK</code> allows both and needs an upper bound:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span> <span class="n">ACYCLIC</span><span class="w"> </span><span class="ss">(</span><span class="py">a:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Alice'</span><span class="ss">})</span><span class="o">-</span><span class="ss">[</span><span class="nc">:KNOWS</span><span class="o">*</span><span class="m">1</span><span class="o">..</span><span class="m">5</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">(</span><span class="n">b</span><span class="ss">)</span>
<span class="k">RETURN</span> <span class="n">b.name</span>
</code></pre></div></div>

<p>This query is also an example of mixing: a GQL path mode in front of a Cypher variable-length relationship.</p>

<h3 id="filter-for-and-finish">FILTER, FOR and FINISH</h3>

<p>GQL’s linear composition statements are supported:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">n:</span><span class="n">Person</span><span class="ss">)</span>
<span class="nf">FILTER</span><span class="w"> </span><span class="ss">(</span><span class="n">n.age</span> <span class="o">&gt;</span> <span class="mi">30</span><span class="ss">)</span>
<span class="k">RETURN</span> <span class="n">n.name</span> <span class="k">AS</span> <span class="n">name</span>
</code></pre></div></div>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">FOR</span> <span class="n">name</span> <span class="ow">IN</span> <span class="ss">[</span><span class="s1">'Alice'</span><span class="ss">,</span> <span class="s1">'Bob'</span><span class="ss">,</span> <span class="s1">'Carol'</span><span class="ss">]</span>
<span class="k">CREATE</span><span class="w"> </span><span class="ss">(</span><span class="nc">:Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="n">name</span><span class="ss">})</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">FINISH</code> ends a query without returning rows, which is handy for writes:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">INSERT</span><span class="w"> </span><span class="ss">(</span><span class="py">n:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Bob'</span><span class="ss">})</span> <span class="n">FINISH</span>
</code></pre></div></div>

<h3 id="insert-and-the-rest-of-data-modification">INSERT and the rest of data modification</h3>

<p>GQL <code class="language-plaintext highlighter-rouge">INSERT</code> takes a pattern, edges included:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">a:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Alice'</span><span class="ss">}),</span> <span class="ss">(</span><span class="py">b:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Bob'</span><span class="ss">})</span>
<span class="n">INSERT</span><span class="w"> </span><span class="ss">(</span><span class="n">a</span><span class="ss">)</span><span class="o">-</span><span class="ss">[</span><span class="py">r:</span><span class="n">KNOWS</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">(</span><span class="n">b</span><span class="ss">)</span>
<span class="k">RETURN</span> <span class="n">r</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SET</code> (labels included), <code class="language-plaintext highlighter-rouge">REMOVE</code>, <code class="language-plaintext highlighter-rouge">DELETE</code>, <code class="language-plaintext highlighter-rouge">DETACH DELETE</code> and <code class="language-plaintext highlighter-rouge">NODETACH DELETE</code> are all supported. Deleting a node that still has edges is refused unless you detach it.</p>

<h3 id="transactions-and-session-parameters">Transactions and session parameters</h3>

<p>GQL transaction statements run as plain statements:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">START</span> <span class="n">TRANSACTION</span>
</code></pre></div></div>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">COMMIT</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ROLLBACK</code> works too, and so does choosing an isolation level with <code class="language-plaintext highlighter-rouge">START TRANSACTION ISOLATION READ_COMMITTED</code> (an ArcadeDB extension). Session parameters work over server sessions, HTTP or Bolt:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SESSION</span> <span class="k">SET</span> <span class="n">$base</span> <span class="o">=</span> <span class="mi">10</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SESSION RESET</code> and <code class="language-plaintext highlighter-rouge">SESSION CLOSE</code> are supported as well. These are not available in embedded mode yet.</p>

<h2 id="what-does-not-work-yet">What does not work yet</h2>

<p>We would rather you find the gaps here than in production. Not supported in ArcadeDB 26.10.x:</p>

<ul>
  <li><strong>Search prefixes <code class="language-plaintext highlighter-rouge">ANY</code> and <code class="language-plaintext highlighter-rouge">SHORTEST</code>.</strong> The parser accepts them but does not apply them yet, so a query using them will not return shortest paths. Use <code class="language-plaintext highlighter-rouge">shortestPath()</code> and <code class="language-plaintext highlighter-rouge">allShortestPaths()</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">SIMPLE</code> path mode and <code class="language-plaintext highlighter-rouge">REPEATABLE ELEMENTS</code>.</strong></li>
  <li><strong><code class="language-plaintext highlighter-rouge">LET</code>, <code class="language-plaintext highlighter-rouge">NEXT</code>, <code class="language-plaintext highlighter-rouge">EXCEPT</code>, <code class="language-plaintext highlighter-rouge">INTERSECT</code>, <code class="language-plaintext highlighter-rouge">OTHERWISE</code> and <code class="language-plaintext highlighter-rouge">USE</code> graph.</strong> Use <code class="language-plaintext highlighter-rouge">WITH</code> where you would use <code class="language-plaintext highlighter-rouge">LET</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CAST</code>.</strong> Use the <code class="language-plaintext highlighter-rouge">toInteger()</code>, <code class="language-plaintext highlighter-rouge">toFloat()</code>, <code class="language-plaintext highlighter-rouge">toString()</code> family instead.</li>
  <li><strong>Unsigned integers, <code class="language-plaintext highlighter-rouge">DECIMAL</code>, <code class="language-plaintext highlighter-rouge">BYTES</code> and <code class="language-plaintext highlighter-rouge">RECORD</code> types.</strong> Use maps instead of records.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">CREATE GRAPH</code>, GQL graph types and <code class="language-plaintext highlighter-rouge">GQLSTATUS</code> codes.</strong> One ArcadeDB database is one graph, the schema is ArcadeDB’s native schema, and errors over Bolt use Neo4j-style <code class="language-plaintext highlighter-rouge">Neo.ClientError.*</code> codes.</li>
</ul>

<p>A few features are partial, such as quantified path patterns (fixed-length groups work, variable-length relationships inside a group do not) and <code class="language-plaintext highlighter-rouge">IS TYPED</code>. The <a href="https://docs.arcadedb.com/arcadedb/reference/cypher/cypher-gql-conformance">coverage statement</a> has the details, and the open items are tracked in <a href="https://github.com/ArcadeData/arcadedb/issues/9940">issue #9940</a>.</p>

<h2 id="why-a-coverage-statement-and-not-a-conformance-score">Why a coverage statement and not a conformance score</h2>

<p>There is no public conformance test suite for ISO GQL yet. Without one, a database can only describe what it implements, so “GQL compliant” is not a claim anyone can check today. We publish the feature list instead.</p>

<p>The engine underneath does have a test score: the Cypher engine runs the openCypher Technology Compatibility Kit on every build and passes 97.8% of its 3,897 scenarios, with zero failures and the rest skipped.</p>

<h2 id="arcadedb-vs-neo4j-on-gql">ArcadeDB vs Neo4j on GQL</h2>

<p>Neo4j also runs GQL inside its Cypher engine, and its <a href="https://neo4j.com/docs/cypher-manual/25/appendix/gql-conformance/">GQL conformance appendix</a> says Cypher supports most of the mandatory GQL features. One difference worth knowing if you are planning a move: Neo4j lists the GQL session and transaction statements (<code class="language-plaintext highlighter-rouge">SESSION SET</code>, <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>) among the <a href="https://neo4j.com/docs/cypher-manual/25/appendix/gql-conformance/unsupported-mandatory/">mandatory features not yet in Cypher</a> and handles them through its driver API. In ArcadeDB they are query statements.</p>

<p>For everything else about moving from Neo4j, including the Bolt protocol, the importer and how your existing Cypher runs unchanged, see the <a href="/neo4j.html">Neo4j to ArcadeDB migration guide</a>.</p>

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

<p>Start ArcadeDB with Docker and open the Studio at <code class="language-plaintext highlighter-rouge">http://localhost:2480</code>:</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">-e</span> <span class="nv">JAVA_OPTS</span><span class="o">=</span><span class="s2">"-Darcadedb.server.rootPassword=playwithdata"</span> arcadedata/arcadedb:26.10.1
</code></pre></div></div>

<p>Pick Cypher as the language and any query in this post will run as written. If a GQL feature you need is on the missing list, tell us on <a href="https://github.com/ArcadeData/arcadedb/issues/9940">GitHub</a>: that list is what decides what we build next.</p>]]></content><author><name>Luca Garulli</name></author><category term="GQL" /><category term="ISO GQL" /><category term="Cypher" /><category term="OpenCypher" /><category term="Graph Query Language" /><category term="Neo4j" /><category term="Graph Database" /><summary type="html"><![CDATA[ArcadeDB runs ISO GQL syntax inside its native Cypher 25 engine. Here is what works today, what does not yet, and how GQL and Cypher mix in one query.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/ISOGQLjpg.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/ISOGQLjpg.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Graph Database vs Relational Database: Differences and When to Use Each</title><link href="https://arcadedb.com/blog/graph-database-vs-relational-database/" rel="alternate" type="text/html" title="Graph Database vs Relational Database: Differences and When to Use Each" /><published>2026-10-12T00:00:00+00:00</published><updated>2026-10-12T00:00:00+00:00</updated><id>https://arcadedb.com/blog/graph-database-vs-relational-database</id><content type="html" xml:base="https://arcadedb.com/blog/graph-database-vs-relational-database/"><![CDATA[<p>“Should we use a graph database or a relational database?” is one of the most common architecture questions, and it is often answered with slogans: graphs are for connected data, relational is for everything else. The useful answer is more specific. It comes down to <strong>how each model stores relationships</strong>, and therefore which queries each one makes cheap.</p>

<p>This post compares the two models on data modeling, querying, performance and operations, with runnable examples, and ends with a short checklist for choosing.</p>

<h2 id="the-short-answer">The short answer</h2>

<ul>
  <li>A <strong>relational database</strong> stores entities as rows in tables. Relationships are values (foreign keys) that a query matches up at run time with a <strong>join</strong>.</li>
  <li>A <strong>graph database</strong> stores entities as vertices and relationships as <strong>edges</strong> between them. A query <strong>traverses</strong> stored edges instead of computing matches.</li>
</ul>

<p>If your important questions are about relationships, especially several hops deep or to an unknown depth, a graph database fits. If they are about sets of rows (totals, filters, reports) with shallow relationships, relational fits. Many applications have both kinds of question, which is why a database that offers both models in one engine is worth knowing about.</p>

<h2 id="data-model-tables-and-keys-vs-vertices-and-edges">Data model: tables and keys vs vertices and edges</h2>

<p>Take a small social network: people who know each other.</p>

<p><strong>Relational model.</strong> Two tables. The relationship is a table of key pairs:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">person</span> <span class="p">(</span><span class="n">id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="n">name</span> <span class="nb">TEXT</span><span class="p">);</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">knows</span>  <span class="p">(</span><span class="n">from_id</span> <span class="nb">INTEGER</span> <span class="k">REFERENCES</span> <span class="n">person</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
                     <span class="n">to_id</span>   <span class="nb">INTEGER</span> <span class="k">REFERENCES</span> <span class="n">person</span><span class="p">(</span><span class="n">id</span><span class="p">));</span>
</code></pre></div></div>

<p>A many-to-many relationship always needs a join table like <code class="language-plaintext highlighter-rouge">knows</code>. Any property of the relationship (when they met, how strong the tie is) becomes a column on that table.</p>

<p><strong>Graph model.</strong> Two types: a vertex type for people and an edge type for the relationship. Properties can live on either.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="k">TYPE</span> <span class="n">Person</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="n">EDGE</span> <span class="k">TYPE</span> <span class="n">Knows</span><span class="p">;</span>

<span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="n">Person</span> <span class="k">SET</span> <span class="n">name</span> <span class="o">=</span> <span class="s1">'Alice'</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="n">Person</span> <span class="k">SET</span> <span class="n">name</span> <span class="o">=</span> <span class="s1">'Bob'</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="n">EDGE</span> <span class="n">Knows</span>
  <span class="k">FROM</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">FROM</span> <span class="n">Person</span> <span class="k">WHERE</span> <span class="n">name</span> <span class="o">=</span> <span class="s1">'Alice'</span><span class="p">)</span>
  <span class="k">TO</span>   <span class="p">(</span><span class="k">SELECT</span> <span class="k">FROM</span> <span class="n">Person</span> <span class="k">WHERE</span> <span class="n">name</span> <span class="o">=</span> <span class="s1">'Bob'</span><span class="p">)</span>
  <span class="k">SET</span> <span class="n">since</span> <span class="o">=</span> <span class="mi">2020</span><span class="p">;</span>
</code></pre></div></div>

<p>That is SQL, running on <a href="https://arcadedb.com">ArcadeDB</a>, which supports both SQL and the graph model. The edge is a record of its own, stored with links to the two vertices it connects, so the database never has to search for who Alice knows: it reads her edges.</p>

<h2 id="querying-joins-vs-traversals">Querying: joins vs traversals</h2>

<p><strong>Friends of friends</strong> (two hops) in relational SQL takes two joins on the link table plus joins back to <code class="language-plaintext highlighter-rouge">person</code>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">DISTINCT</span> <span class="n">p3</span><span class="p">.</span><span class="n">name</span>
<span class="k">FROM</span> <span class="n">person</span> <span class="n">p1</span>
<span class="k">JOIN</span> <span class="n">knows</span> <span class="n">k1</span>  <span class="k">ON</span> <span class="n">k1</span><span class="p">.</span><span class="n">from_id</span> <span class="o">=</span> <span class="n">p1</span><span class="p">.</span><span class="n">id</span>
<span class="k">JOIN</span> <span class="n">knows</span> <span class="n">k2</span>  <span class="k">ON</span> <span class="n">k2</span><span class="p">.</span><span class="n">from_id</span> <span class="o">=</span> <span class="n">k1</span><span class="p">.</span><span class="n">to_id</span>
<span class="k">JOIN</span> <span class="n">person</span> <span class="n">p3</span> <span class="k">ON</span> <span class="n">p3</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">k2</span><span class="p">.</span><span class="n">to_id</span>
<span class="k">WHERE</span> <span class="n">p1</span><span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="s1">'Alice'</span> <span class="k">AND</span> <span class="n">p3</span><span class="p">.</span><span class="n">id</span> <span class="o">&lt;&gt;</span> <span class="n">p1</span><span class="p">.</span><span class="n">id</span><span class="p">;</span>
</code></pre></div></div>

<p>In Cypher, the graph query language most graph databases support, the same question describes the path:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">a:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Alice'</span><span class="ss">})</span><span class="o">-</span><span class="ss">[</span><span class="nc">:Knows</span><span class="o">*</span><span class="mi">2</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">(</span><span class="n">fof</span><span class="ss">)</span>
<span class="k">WHERE</span> <span class="n">fof</span> <span class="o">&lt;&gt;</span> <span class="n">a</span>
<span class="k">RETURN</span> <span class="k">DISTINCT</span> <span class="n">fof.name</span>
</code></pre></div></div>

<p><strong>Unknown depth</strong> is where the models really diverge. “Everyone Alice can reach through any chain of acquaintances” needs a recursive common table expression in SQL, which repeats a join on every iteration:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="k">RECURSIVE</span> <span class="n">reach</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">depth</span><span class="p">)</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="n">k</span><span class="p">.</span><span class="n">to_id</span><span class="p">,</span> <span class="mi">1</span>
  <span class="k">FROM</span> <span class="n">knows</span> <span class="n">k</span> <span class="k">JOIN</span> <span class="n">person</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">p</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">k</span><span class="p">.</span><span class="n">from_id</span>
  <span class="k">WHERE</span> <span class="n">p</span><span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="s1">'Alice'</span>
  <span class="k">UNION</span>
  <span class="k">SELECT</span> <span class="n">k</span><span class="p">.</span><span class="n">to_id</span><span class="p">,</span> <span class="n">r</span><span class="p">.</span><span class="n">depth</span> <span class="o">+</span> <span class="mi">1</span>
  <span class="k">FROM</span> <span class="n">reach</span> <span class="n">r</span> <span class="k">JOIN</span> <span class="n">knows</span> <span class="n">k</span> <span class="k">ON</span> <span class="n">k</span><span class="p">.</span><span class="n">from_id</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">id</span>
  <span class="k">WHERE</span> <span class="n">r</span><span class="p">.</span><span class="n">depth</span> <span class="o">&lt;</span> <span class="mi">5</span>
<span class="p">)</span>
<span class="k">SELECT</span> <span class="k">DISTINCT</span> <span class="n">p</span><span class="p">.</span><span class="n">name</span>
<span class="k">FROM</span> <span class="n">reach</span> <span class="n">r</span> <span class="k">JOIN</span> <span class="n">person</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">p</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">id</span>
<span class="k">WHERE</span> <span class="n">p</span><span class="p">.</span><span class="n">name</span> <span class="o">&lt;&gt;</span> <span class="s1">'Alice'</span><span class="p">;</span>
</code></pre></div></div>

<p>In a graph database it is a variable-length pattern:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="py">a:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Alice'</span><span class="ss">})</span><span class="o">-</span><span class="ss">[</span><span class="nc">:Knows</span><span class="o">*</span><span class="m">1</span><span class="o">..</span><span class="m">5</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">(</span><span class="n">b</span><span class="ss">)</span>
<span class="k">RETURN</span> <span class="k">DISTINCT</span> <span class="n">b.name</span>
</code></pre></div></div>

<p>And <strong>shortest path</strong> is a built-in function rather than an exercise in recursive SQL:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">MATCH</span> <span class="n">p</span> <span class="o">=</span> <span class="nf">shortestPath</span><span class="ss">(</span>
  <span class="ss">(</span><span class="py">a:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Alice'</span><span class="ss">})</span><span class="o">-</span><span class="ss">[</span><span class="nc">:Knows</span><span class="o">*</span><span class="ss">]</span><span class="o">-</span><span class="ss">(</span><span class="py">b:</span><span class="n">Person</span> <span class="ss">{</span><span class="py">name:</span> <span class="s1">'Dave'</span><span class="ss">}))</span>
<span class="k">RETURN</span> <span class="ss">[</span><span class="n">n</span> <span class="ow">IN</span> <span class="nf">nodes</span><span class="ss">(</span><span class="n">p</span><span class="ss">)</span> <span class="o">|</span> <span class="n">n.name</span><span class="ss">]</span> <span class="k">AS</span> <span class="n">names</span>
</code></pre></div></div>

<p>The <a href="/cheat-sheet.html">ArcadeDB query cheat sheet</a> has these patterns and more in SQL, Cypher and Gremlin side by side.</p>

<h2 id="performance-why-hops-change-everything">Performance: why hops change everything</h2>

<p>A join is computed: for each row on one side, the database finds matching rows on the other, typically through an index. Each additional hop is another join over the intermediate result, so the work grows with both the depth and the size of the tables involved.</p>

<p>A traversal reads relationships that are already stored with the record. The cost depends on how many edges the query actually touches, not on how large the tables are overall. That is why graph databases pull ahead on deep, branching queries and why the gap widens as hops increase.</p>

<p>We measured this directly on the LDBC Labelled Subgraph Query Benchmark, comparing ArcadeDB to PostgreSQL: across all nine queries ArcadeDB finished in 2.2 seconds against 69 seconds for PostgreSQL, and gaps of more than 150x on the two-hop friends-of-friends traversal and the star-shaped patterns. The full method, the queries and the caveats are in <a href="/blog/arcadedb-vs-postgresql-graph-queries-sql-pgq/">ArcadeDB vs PostgreSQL for graph queries</a>.</p>

<p>The flip side is just as real: for scanning and aggregating one large table, or fetching a row by primary key, a relational database is excellent, and a graph database has no inherent advantage.</p>

<h2 id="schema-and-flexibility">Schema and flexibility</h2>

<p>Relational databases enforce a schema up front: every row of a table has the same columns, and changing the shape means a migration. That rigidity is a feature when data is regular and many applications share it.</p>

<p>Graph databases tend to be more flexible. In ArcadeDB, for example, Cypher and Gremlin create a type the first time you use a label, while SQL can declare properties, constraints and unique indexes when you want them enforced. You can start loose and tighten the schema as the model settles. Adding a new kind of relationship is a new edge type, not a new join table and a migration.</p>

<h2 id="transactions-tooling-and-maturity">Transactions, tooling and maturity</h2>

<p>Both families include ACID databases; ArcadeDB is fully ACID, and so are the major relational systems. Check this per product rather than per category.</p>

<p>Relational databases have decades of tooling: BI tools, ORMs, and a large pool of people who know SQL. Graph tooling is younger. Two things narrow that gap: graph databases that also speak SQL, and graph databases that implement the PostgreSQL wire protocol so existing drivers and tools can connect. ArcadeDB does both.</p>

<h2 id="when-to-use-a-relational-database">When to use a relational database</h2>

<ul>
  <li>Data is naturally <strong>tabular</strong> and regular.</li>
  <li>Relationships are <strong>shallow</strong>: one or two joins answer your questions.</li>
  <li>The workload is mostly <strong>aggregations, reports and CRUD</strong> by key.</li>
  <li>Your application depends on a specific relational product’s SQL dialect, stored procedures or extensions.</li>
</ul>

<h2 id="when-to-use-a-graph-database">When to use a graph database</h2>

<ul>
  <li>The value is in the <strong>connections</strong>: <a href="/fraud-detection.html">fraud detection</a> rings, <a href="/recommendation-engine.html">recommendation engines</a> that walk from users to items to similar users, <a href="/iam.html">identity and access management</a> where permissions inherit through groups and roles.</li>
  <li>Queries need <strong>variable depth</strong> or <strong>paths</strong>: lineage, supply chains, network impact analysis.</li>
  <li>The model <strong>changes often</strong> and new relationship types appear over time, as in <a href="/knowledge-graphs.html">knowledge graphs</a>.</li>
  <li>You are building <strong>AI retrieval</strong> that combines vector similarity with relationships, such as GraphRAG.</li>
</ul>

<h2 id="you-do-not-always-have-to-choose">You do not always have to choose</h2>

<p>Many systems have both kinds of question: an order table that needs monthly reports and a customer graph that needs traversals. The traditional answer was two databases and a pipeline to keep them in sync.</p>

<p>A multi-model database such as ArcadeDB stores tabular records, documents and graph edges in one engine, under one transaction, and lets you query them with SQL for reporting and Cypher or Gremlin for traversals. Our guide to <a href="/blog/what-is-a-multi-model-database/">what a multi-model database is</a> explains the trade-offs of that approach.</p>

<p>ArcadeDB is open source under the Apache 2.0 license. The free <a href="/academy.html">ArcadeDB Academy</a> has courses on both SQL and Cypher if you want to try the examples above hands-on.</p>]]></content><author><name>Roberto Franchini</name></author><category term="Graph Database" /><category term="Relational Database" /><category term="SQL" /><category term="Cypher" /><category term="Data Modeling" /><category term="Comparison" /><category term="Graph vs Relational" /><category term="Explainer" /><summary type="html"><![CDATA[Graph database vs relational database: how each stores relationships, how queries differ (joins vs traversals), where each one wins, and how to get both models in one engine.]]></summary></entry><entry><title type="html">What Is a Multi-Model Database? A Practical Guide</title><link href="https://arcadedb.com/blog/what-is-a-multi-model-database/" rel="alternate" type="text/html" title="What Is a Multi-Model Database? A Practical Guide" /><published>2026-10-12T00:00:00+00:00</published><updated>2026-10-12T00:00:00+00:00</updated><id>https://arcadedb.com/blog/what-is-a-multi-model-database</id><content type="html" xml:base="https://arcadedb.com/blog/what-is-a-multi-model-database/"><![CDATA[<p>Most applications outgrow a single data shape. Orders are rows, product catalogs are nested documents, customers and their relationships form a graph, search needs an inverted index, and more and more teams are asked to add embeddings for semantic search. The traditional answer was one specialized database per shape. A <strong>multi-model database</strong> answers the same need with one engine.</p>

<p>This guide explains what a multi-model database is, how it differs from running several databases side by side, where it fits and where it does not, with examples in <a href="https://arcadedb.com">ArcadeDB</a>, an open-source multi-model database.</p>

<h2 id="what-is-a-multi-model-database">What is a multi-model database?</h2>

<p>A multi-model database is a single database engine that supports several data models natively and lets you query them together. Instead of choosing between a relational, document, graph or vector database, you get the models you need in one place, with one storage engine, one transaction manager, one security model and one backup.</p>

<p>The models you will meet most often:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Shape of the data</th>
      <th>Typical questions</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Relational / tabular</strong></td>
      <td>Rows with typed columns</td>
      <td>Totals, filters, reports</td>
    </tr>
    <tr>
      <td><strong>Document</strong></td>
      <td>Nested JSON objects and arrays</td>
      <td>Store an entity as one record, flexible fields</td>
    </tr>
    <tr>
      <td><strong>Graph</strong></td>
      <td>Vertices connected by edges, both with properties</td>
      <td>Paths, neighbors, influence, rings, recommendations</td>
    </tr>
    <tr>
      <td><strong>Key-value</strong></td>
      <td>A value looked up by key</td>
      <td>Caches, sessions, counters</td>
    </tr>
    <tr>
      <td><strong>Full-text search</strong></td>
      <td>Inverted index over text</td>
      <td>Keyword search with relevance ranking</td>
    </tr>
    <tr>
      <td><strong>Vector</strong></td>
      <td>Embeddings indexed for nearest-neighbor search</td>
      <td>Semantic search, RAG retrieval, similarity</td>
    </tr>
    <tr>
      <td><strong>Time series</strong></td>
      <td>Timestamped measurements</td>
      <td>Metrics, sensor data, rollups over time windows</td>
    </tr>
  </tbody>
</table>

<p>A database is multi-model when several of these are first-class: you can create them, index them and query them without bolting on a second system.</p>

<h2 id="multi-model-vs-polyglot-persistence">Multi-model vs polyglot persistence</h2>

<p>The alternative to multi-model is <strong>polyglot persistence</strong>: one specialized database per model. A typical stack might pair a relational database for orders, a document store for the catalog, a graph database for relationships, a search engine for text and a vector database for embeddings.</p>

<p>Each of those products may be excellent at its job. The cost is in the seams between them:</p>

<ul>
  <li><strong>Synchronization.</strong> The same customer now lives in several systems. Every write has to reach all of them, usually through change-data-capture or a message queue, and every pipeline is another thing that can lag or break.</li>
  <li><strong>No shared transaction.</strong> If an order is written to one store and the relationship to another, there is no single commit. Failures leave the systems disagreeing until something reconciles them.</li>
  <li><strong>Queries that cross models become application code.</strong> “Find documents semantically similar to this question, then follow the graph to the entities they mention” turns into two network calls and a join in your service.</li>
  <li><strong>Operations multiply.</strong> Each database has its own backups, upgrades, monitoring, security model and expertise to hire for.</li>
</ul>

<p>A multi-model database removes the seams: the records, edges, text index and vectors live in one engine and commit together.</p>

<h2 id="what-multi-model-looks-like-in-practice">What multi-model looks like in practice</h2>

<p>In ArcadeDB, a vertex is also a document. It can hold nested objects and arrays, sit in a graph, and carry an embedding, all in one record:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="k">TYPE</span> <span class="n">Customer</span><span class="p">;</span>

<span class="k">CREATE</span> <span class="n">VERTEX</span> <span class="n">Customer</span> <span class="n">CONTENT</span> <span class="p">{</span>
  <span class="nv">"name"</span><span class="p">:</span> <span class="nv">"Acme"</span><span class="p">,</span>
  <span class="nv">"address"</span><span class="p">:</span> <span class="p">{</span> <span class="nv">"city"</span><span class="p">:</span> <span class="nv">"Rome"</span><span class="p">,</span> <span class="nv">"zip"</span><span class="p">:</span> <span class="nv">"00100"</span> <span class="p">},</span>
  <span class="nv">"tags"</span><span class="p">:</span> <span class="p">[</span><span class="nv">"vip"</span><span class="p">,</span> <span class="nv">"eu"</span><span class="p">]</span>
<span class="p">};</span>

<span class="c1">-- document-style query on nested fields and arrays</span>
<span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="n">address</span><span class="p">.</span><span class="n">city</span> <span class="k">AS</span> <span class="n">city</span>
<span class="k">FROM</span> <span class="n">Customer</span>
<span class="k">WHERE</span> <span class="s1">'vip'</span> <span class="k">IN</span> <span class="n">tags</span><span class="p">;</span>
</code></pre></div></div>

<p>The same record can then be connected with edges and traversed as a graph. And because the vector index lives in the same engine, one query can retrieve by meaning and expand by relationships. This Cypher query finds the documents closest to a question embedding and returns the entities each one mentions:</p>

<div class="language-cypher highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CALL</span> <span class="n">db.index.vector.queryNodes</span><span class="ss">(</span><span class="s1">'Doc[embedding]'</span><span class="ss">,</span> <span class="mi">5</span><span class="ss">,</span> <span class="n">$queryVector</span><span class="ss">)</span>
<span class="k">YIELD</span> <span class="n">node</span><span class="ss">,</span> <span class="n">score</span>
<span class="k">MATCH</span><span class="w"> </span><span class="ss">(</span><span class="n">node</span><span class="ss">)</span><span class="o">-</span><span class="ss">[</span><span class="nc">:Mentions</span><span class="ss">]</span><span class="o">-&gt;</span><span class="ss">(</span><span class="py">e:</span><span class="n">Entity</span><span class="ss">)</span>
<span class="k">RETURN</span> <span class="n">node.title</span> <span class="k">AS</span> <span class="n">title</span><span class="ss">,</span> <span class="n">score</span><span class="ss">,</span> <span class="nf">collect</span><span class="ss">(</span><span class="n">e.name</span><span class="ss">)</span> <span class="k">AS</span> <span class="n">entities</span>
</code></pre></div></div>

<p>With polyglot persistence, that is a call to a vector database, a list of IDs, and a second call to a graph database. In one engine it is one query and one round trip. It is also the core pattern behind <a href="/graph-rag.html">GraphRAG</a>: vector search finds the entry points, the graph supplies the context an LLM needs.</p>

<p>Several query languages over the same data is the other half of multi-model. ArcadeDB accepts SQL, Cypher, Gremlin, GraphQL, and the MongoDB and Redis query languages against the same records, so each team can use the language that suits the task. Our <a href="/cheat-sheet.html">query cheat sheet</a> shows the same tasks in SQL, Cypher and Gremlin side by side.</p>

<h2 id="native-vs-layered-multi-model">Native vs layered multi-model</h2>

<p>“Multi-model” covers different architectures, and the difference matters for performance.</p>

<ul>
  <li><strong>Native multi-model.</strong> The engine implements each model directly: graph edges are stored as links between records, so following a relationship does not require a join or an index lookup per hop; documents are stored as documents; vectors live in a real approximate-nearest-neighbor index.</li>
  <li><strong>Layered or emulated.</strong> One core model hosts the others. A relational database might store documents in a JSON column and answer graph queries by translating them into joins. That works for light use, but a deep traversal still pays the cost of a join per hop.</li>
</ul>

<p>When you evaluate a product, ask how each model you care about is stored and executed, not only whether it appears on the feature list. Our comparison of <a href="/blog/arcadedb-vs-postgresql-graph-queries-sql-pgq/">ArcadeDB and PostgreSQL for graph queries</a> shows the difference on a standard benchmark: graph patterns that compile to joins against the same patterns run on native graph storage.</p>

<h2 id="benefits-of-a-multi-model-database">Benefits of a multi-model database</h2>

<ul>
  <li><strong>One transaction across models.</strong> A new customer, the edge to their company and their embedding commit together or not at all.</li>
  <li><strong>No synchronization pipeline.</strong> There is one copy of the data, so nothing can drift.</li>
  <li><strong>Cross-model queries.</strong> Filter on document fields, rank by text relevance or vector similarity, and traverse relationships in the same query.</li>
  <li><strong>Fewer systems to run.</strong> One deployment, one backup, one monitoring setup, one set of credentials and permissions.</li>
  <li><strong>Room to evolve.</strong> When the product needs a graph or semantic search next year, you add a type and an index, not a new database.</li>
</ul>

<h2 id="trade-offs-to-consider">Trade-offs to consider</h2>

<p>Multi-model is not automatically the right answer.</p>

<ul>
  <li><strong>Depth per model varies by product.</strong> A specialized engine may offer niche features a multi-model engine lacks. List the features you actually use and check each one.</li>
  <li><strong>One engine is one failure domain.</strong> Consolidation reduces moving parts, but it also means the database’s high-availability setup carries more weight. Look at replication and failover as carefully as at features.</li>
  <li><strong>Team familiarity.</strong> If your team knows one specialized database deeply and your application only ever needs that model, switching buys little.</li>
</ul>

<h2 id="when-to-choose-a-multi-model-database">When to choose a multi-model database</h2>

<p>A multi-model database is a strong fit when:</p>

<ul>
  <li>your data has <strong>relationships that matter</strong> and also needs document flexibility or search, as in a <a href="/customer-360.html">Customer 360</a> view that unifies profiles, interactions and accounts;</li>
  <li>you are building <strong>AI features</strong> that combine vector retrieval with structured context, such as GraphRAG or agent memory;</li>
  <li>you need <strong>consistency across shapes</strong>, as in <a href="/fraud-detection.html">fraud detection</a>, where transactions, devices, accounts and their links must be evaluated together in real time;</li>
  <li>you want to <strong>cut the number of databases</strong> you operate.</li>
</ul>

<p>If you already run a database and want to see how a multi-model engine compares, the <a href="/blog/neo4j-alternatives-in-2026-a-fair-look-at-the-open-source-options/">Neo4j alternatives overview</a> covers the open-source options, and ArcadeDB’s <a href="/orientdb.html">OrientDB migration guide</a> covers the multi-model database it succeeded.</p>

<h2 id="try-arcadedb">Try ArcadeDB</h2>

<p>ArcadeDB is open source under the Apache 2.0 license, with graph, document, key-value, full-text search, vector and time-series models in one engine. You can run it <a href="/embedded.html">embedded in a Java application</a>, as a server, or as a cluster. The free <a href="/academy.html">ArcadeDB Academy</a> courses walk through the models one at a time, and the <a href="https://github.com/ArcadeData/arcadedb">GitHub repository</a> has everything else.</p>]]></content><author><name>Roberto Franchini</name></author><category term="Multi-Model" /><category term="Multi-Model Database" /><category term="Graph Database" /><category term="Document Database" /><category term="Vector Search" /><category term="Polyglot Persistence" /><category term="Database Architecture" /><category term="Explainer" /><summary type="html"><![CDATA[A multi-model database stores graphs, documents, key-value pairs, vectors and time series in one engine, under one transaction. What it is, how it differs from polyglot persistence, and when it is the right choice.]]></summary></entry><entry><title type="html">ArcadeDB 26.10.1: HA Hardening, Lost-Update Fixes, Consistent Equality, and 4 Security Advisories</title><link href="https://arcadedb.com/blog/arcadedb-26-10-1/" rel="alternate" type="text/html" title="ArcadeDB 26.10.1: HA Hardening, Lost-Update Fixes, Consistent Equality, and 4 Security Advisories" /><published>2026-10-05T00:00:00+00:00</published><updated>2026-10-05T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-26-10-1</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-26-10-1/"><![CDATA[<p><strong>ArcadeDB 26.10.1</strong> closes <strong>1,615 issues and pull requests</strong> under the <a href="https://github.com/ArcadeData/arcadedb/milestone/61">26.10.1 milestone</a>: 1,244 issues and 371 PRs, out of 846 pull requests merged and about <strong>3,100 commits</strong> since <a href="https://arcadedb.com/blog/arcadedb-26-9-1/">26.9.1</a>. <strong>984 of the closed issues are bug fixes, 75 of them rated critical.</strong></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>High availability (Raft)</td>
      <td>Snapshot install, follower catch-up, leadership hand-off, readiness, restart recovery</td>
      <td>~<strong>300</strong> issues with the <code class="language-plaintext highlighter-rouge">ha</code> label</td>
    </tr>
    <tr>
      <td>Silent data loss</td>
      <td>Lost updates on growing records, hash-index key changes applied twice, stale-read writes</td>
      <td>most concurrent-growth runs lost updates before</td>
    </tr>
    <tr>
      <td>Equality</td>
      <td>Index, SQL scan and Cypher disagreed for FLOAT, DECIMAL, DATETIME, BOOLEAN, null, signed zero</td>
      <td>one answer everywhere</td>
    </tr>
    <tr>
      <td>Query speed</td>
      <td>Parallel scans, <code class="language-plaintext highlighter-rouge">DISTINCT</code> and aggregation, index-ordered <code class="language-plaintext highlighter-rouge">LIMIT</code>, striped database lock</td>
      <td>see below</td>
    </tr>
    <tr>
      <td>Wire protocols</td>
      <td>Postgres, MongoDB, Redis, Bolt, gRPC, Gremlin</td>
      <td><strong>100+</strong> fixes, plus Postgres TLS</td>
    </tr>
    <tr>
      <td>Security</td>
      <td>4 advisories, one critical</td>
      <td>all affecting <strong>26.9.1 and earlier</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Upgrading is recommended for every deployment, and in particular for every HA cluster.</strong> Behaviour changes are collected in the <a href="#upgrade-checklist">upgrade checklist</a> below.</p>

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

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

<ul>
  <li><strong>TLS/SSL for the Postgres wire protocol</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/8840">#8840</a>).</li>
  <li><strong>Opt-in OTLP log export</strong>, an OTLP/HTTP metrics endpoint fix, and <code class="language-plaintext highlighter-rouge">service.name</code> in tracing (<a href="https://github.com/ArcadeData/arcadedb/pull/7727">#7727</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7294">#7294</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7295">#7295</a>).</li>
  <li><strong>Persistent instance id</strong> (<code class="language-plaintext highlighter-rouge">adb-&lt;uuid&gt;</code>) shown in Studio, the <code class="language-plaintext highlighter-rouge">arcadedb.support.id</code> setting, and Studio <strong>support requests</strong> that run a read-only query support asked for and send the result back (<a href="https://github.com/ArcadeData/arcadedb/pull/8685">#8685</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/8682">#8682</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/8861">#8861</a>).</li>
  <li><strong>A configurable config directory</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/7415">#7415</a>) and compact object headers enabled by <code class="language-plaintext highlighter-rouge">server.sh</code> when the JVM accepts them (<a href="https://github.com/ArcadeData/arcadedb/issues/4537">#4537</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">HEAD</code> on <code class="language-plaintext highlighter-rouge">/ready</code> and <code class="language-plaintext highlighter-rouge">/health</code></strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/8133">#8133</a>); the HTTP server binds every local address the configured host resolves to (<a href="https://github.com/ArcadeData/arcadedb/issues/8692">#8692</a>).</li>
  <li><strong>Pool metrics</strong> for the per-server security executors (<a href="https://github.com/ArcadeData/arcadedb/issues/7856">#7856</a>).</li>
  <li><strong>Read-your-writes bookmark</strong> (<code class="language-plaintext highlighter-rouge">X-ArcadeDB-Commit-Index</code>) on streamed HTTP responses (<a href="https://github.com/ArcadeData/arcadedb/issues/7351">#7351</a>), and streaming ingestion with a temporary-id mapping returned chunk by chunk (<a href="https://github.com/ArcadeData/arcadedb/issues/7353">#7353</a>).</li>
  <li><strong>Startup <code class="language-plaintext highlighter-rouge">restore:</code></strong> publishes progress and waits out a conflicting operation (<a href="https://github.com/ArcadeData/arcadedb/issues/7440">#7440</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7652">#7652</a>).</li>
</ul>

<h3 id="studio">Studio</h3>

<ul>
  <li><strong>Index search and time series move into Query and Database, with a new Chart tab</strong> (<a href="https://github.com/ArcadeData/arcadedb/pull/8788">#8788</a>).</li>
  <li>A selected saved query is no longer run automatically (<a href="https://github.com/ArcadeData/arcadedb/issues/7049">#7049</a>), and the cluster page shows peer capabilities and rolling-upgrade readiness.</li>
</ul>

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

<ul>
  <li><strong>Property rename</strong> as an in-place primitive, in the Java API and SQL (<a href="https://github.com/ArcadeData/arcadedb/issues/7589">#7589</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">COPY (&lt;query&gt;) TO STDOUT</code></strong> over the Postgres wire, in text and binary format (<a href="https://github.com/ArcadeData/arcadedb/issues/7188">#7188</a>), and NaN-transparent TimeSeries <code class="language-plaintext highlighter-rouge">SUM</code>/<code class="language-plaintext highlighter-rouge">AVG</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/7089">#7089</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">algo.personalizedPageRank</code></strong> takes a personalization vector (<a href="https://github.com/ArcadeData/arcadedb/issues/8715">#8715</a>); Cypher <code class="language-plaintext highlighter-rouge">point()</code> returns Neo4j SRIDs (<a href="https://github.com/ArcadeData/arcadedb/issues/3993">#3993</a>).</li>
  <li><strong>TimeSeries</strong>: bucket origin/offset on <code class="language-plaintext highlighter-rouge">ts.timeBucket</code>, per-column codec syntax in <code class="language-plaintext highlighter-rouge">CREATE TIMESERIES TYPE</code>, and <code class="language-plaintext highlighter-rouge">AggregationMetrics</code> over HTTP (<a href="https://github.com/ArcadeData/arcadedb/issues/8798">#8798</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7690">#7690</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7717">#7717</a>).</li>
  <li><strong>Graph Importer</strong> imports JSON array properties (vector embeddings) from JSONL (<a href="https://github.com/ArcadeData/arcadedb/issues/7185">#7185</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">apoc.merge.node</code>/<code class="language-plaintext highlighter-rouge">apoc.merge.relationship</code> <code class="language-plaintext highlighter-rouge">onMatchProps</code></strong> and <code class="language-plaintext highlighter-rouge">db.index.fulltext.query*</code> options (<a href="https://github.com/ArcadeData/arcadedb/issues/8117">#8117</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8103">#8103</a>). Teams moving from Neo4j can see how much of the surface is covered on the <a href="https://arcadedb.com/neo4j.html">Neo4j alternative page</a>.</li>
</ul>

<h3 id="remote-clients-and-grpc">Remote Clients and gRPC</h3>

<p>A spec-driven client generation epic (<a href="https://github.com/ArcadeData/arcadedb/issues/4894">#4894</a>) closed the gaps between the HTTP, gRPC and Java remote APIs: a gRPC control plane (database lifecycle, backup, security, profiler, restore and import, groups and API tokens), a gRPC time-series API, an HTTP streaming query and vector search, an HTTP/<code class="language-plaintext highlighter-rouge">/ws</code> insert session with a Java client, and <code class="language-plaintext highlighter-rouge">RemoteDatabase</code> access to <code class="language-plaintext highlighter-rouge">/ts/*</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/7304">#7304</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7305">#7305</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7306">#7306</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7307">#7307</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7403">#7403</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7406">#7406</a>).</p>

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

<h3 id="high-availability-a-hardening-pass-on-every-path-a-cluster-takes-to-recover">High Availability: a Hardening Pass on Every Path a Cluster Takes to Recover</h3>

<p>Around 300 issues carry the <code class="language-plaintext highlighter-rouge">ha</code> label this cycle. The ones that mattered most in practice:</p>

<ul>
  <li><strong>A follower stuck at a stale term</strong> after a restart or rolling restart, costing quorum on the next failure, is fixed at the root: the leader looped on install-snapshot notifications for ever after a follower installed at <code class="language-plaintext highlighter-rouge">leaderStart-1</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/8289">#8289</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8341">#8341</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8360">#8360</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8449">#8449</a>).</li>
  <li><strong>Stale entries after a snapshot install.</strong> A follower that re-applied its local log while a leader snapshot arrived applied stale entries onto the fresh copy and halted. Every install path now carries a boundary index, and a leader snapshot that is behind what the follower already applied is refused (<a href="https://github.com/ArcadeData/arcadedb/issues/8577">#8577</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8579">#8579</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8454">#8454</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8651">#8651</a>).</li>
  <li><strong>A resync reopened pre-resync files.</strong> A snapshot resync skipped a database that was closed on the follower, a node could serve a snapshot of a database it had quarantined, and a node elected leader could reopen an unverified closed copy as the cluster’s copy (<a href="https://github.com/ArcadeData/arcadedb/issues/8464">#8464</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8468">#8468</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8605">#8605</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8589">#8589</a>). An audited override now exists for a peer that can never answer (<a href="https://github.com/ArcadeData/arcadedb/issues/8641">#8641</a>).</li>
  <li><strong>Divergence after a graceful restart under load</strong> (extra rows and a corrupt unique LSM index) is closed (<a href="https://github.com/ArcadeData/arcadedb/issues/8270">#8270</a>), and so is a leader DDL that published its pages before its schema entry, so a concurrent replica transaction on the same pages merged against the old schema (<a href="https://github.com/ArcadeData/arcadedb/issues/7438">#7438</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8686">#8686</a>).</li>
  <li><strong>Leadership hand-off.</strong> The automatic hand-offs only screened the target for lag, so a peer that was down was a valid target and the leader then refused every write. Targets are proven reachable now, the budget is sliced per candidate, and a node whose database was dropped for a resync can no longer be elected (<a href="https://github.com/ArcadeData/arcadedb/issues/8556">#8556</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8491">#8491</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8533">#8533</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8480">#8480</a>).</li>
  <li><strong>A write that may have committed was retried.</strong> <code class="language-plaintext highlighter-rouge">MajorityCommittedAllFailedException</code> and <code class="language-plaintext highlighter-rouge">ReplicationDispatchedTimeoutException</code> were <code class="language-plaintext highlighter-rouge">NeedRetryException</code>s, so a committed write could be replayed (<a href="https://github.com/ArcadeData/arcadedb/issues/8481">#8481</a>). The remote client now sends <code class="language-plaintext highlighter-rouge">X-Request-Id</code>, so a write whose response was lost is retried safely (<a href="https://github.com/ArcadeData/arcadedb/issues/8526">#8526</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8136">#8136</a>).</li>
  <li><strong>Readiness is honest.</strong> The security-convergence gate is armed on a runtime join, per join, and on by default; a node re-added with a retained config volume no longer passes it on stale fingerprints (<a href="https://github.com/ArcadeData/arcadedb/issues/7819">#7819</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8317">#8317</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8329">#8329</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8414">#8414</a>). Followers are gated on the leader’s commit index (<a href="https://github.com/ArcadeData/arcadedb/issues/7619">#7619</a>).</li>
  <li><strong>Two concurrent add-peer requests could commit two Raft peer ids for one address</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/7802">#7802</a>), and the embedded <code class="language-plaintext highlighter-rouge">addPeer</code> API seeded no security documents at all (<a href="https://github.com/ArcadeData/arcadedb/issues/7820">#7820</a>).</li>
  <li><strong>A long-running seeded chaos test</strong> now exercises compound failure and recovery scenarios (<a href="https://github.com/ArcadeData/arcadedb/issues/8279">#8279</a>), and the HA integration tests run nightly instead of on every push.</li>
</ul>

<p>New on the HA side: a bulk schema scope so a DDL script replicates as one Raft entry, peer-capability negotiation, peer priority on <code class="language-plaintext highlighter-rouge">POST /api/v1/cluster/peer</code>, joining a running node to a cluster, and per-peer capabilities and rolling-upgrade readiness in Studio (<a href="https://github.com/ArcadeData/arcadedb/issues/6990">#6990</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7219">#7219</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7523">#7523</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7515">#7515</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7538">#7538</a>). If you run ArcadeDB in <a href="https://arcadedb.com/client-server.html">client-server mode</a> with a Raft cluster, this is the release to be on.</p>

<h3 id="lost-updates-and-silent-data-loss">Lost Updates and Silent Data Loss</h3>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">SET n = n + 1</code> lost an increment</strong> under <code class="language-plaintext highlighter-rouge">READ_COMMITTED</code> when two transactions committed concurrently (<a href="https://github.com/ArcadeData/arcadedb/issues/8538">#8538</a>). A write computed from a read that went stale is now refused (<a href="https://github.com/ArcadeData/arcadedb/issues/8610">#8610</a>).</li>
  <li><strong>Concurrent updates of records that grow past a page</strong> lost committed updates in most runs with <code class="language-plaintext highlighter-rouge">txPageSlotMerge</code> on (the default), and a document larger than one page was silently overwritten (<a href="https://github.com/ArcadeData/arcadedb/issues/8985">#8985</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8982">#8982</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/8988">#8988</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/8989">#8989</a>).</li>
  <li><strong>HASH indexes applied a key change twice at commit</strong>: <code class="language-plaintext highlighter-rouge">UNIQUE_HASH</code> let an <code class="language-plaintext highlighter-rouge">UPDATE</code> take a key another record held, and <code class="language-plaintext highlighter-rouge">NOTUNIQUE_HASH</code> kept a stale entry (<a href="https://github.com/ArcadeData/arcadedb/issues/8983">#8983</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">UPDATE ... SET</code> on a nested map key or list index was silently lost</strong>, because the owner was never marked dirty (<a href="https://github.com/ArcadeData/arcadedb/issues/8027">#8027</a>); <code class="language-plaintext highlighter-rouge">UPDATE ... REMOVE prop[0]</code> on a Set deleted every element (<a href="https://github.com/ArcadeData/arcadedb/issues/8032">#8032</a>).</li>
  <li><strong>One interrupted commit permanently killed a WAL pool slot</strong>, so every later transaction on it blocked 30 s and failed (<a href="https://github.com/ArcadeData/arcadedb/issues/7768">#7768</a>).</li>
  <li><strong>A UNIDIRECTIONAL edge created while its target was deleted committed as a ghost edge</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/8986">#8986</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8676">#8676</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">DROP TYPE</code> left the type’s aliases in the schema</strong>, so the dropped type came back alive after a restart (<a href="https://github.com/ArcadeData/arcadedb/issues/8169">#8169</a>); <code class="language-plaintext highlighter-rouge">TRUNCATE TYPE</code> never refused a non-empty vertex or edge type (<a href="https://github.com/ArcadeData/arcadedb/issues/8042">#8042</a>).</li>
  <li><strong>Console data loss.</strong> The console split a command at the first closing brace followed by a newline, so a multi-line <code class="language-plaintext highlighter-rouge">UPDATE ... SET x = {json}</code> ran with no <code class="language-plaintext highlighter-rouge">WHERE</code> and overwrote every row (<a href="https://github.com/ArcadeData/arcadedb/issues/8246">#8246</a>).</li>
  <li><strong>Clean close and WAL.</strong> A clean close now fsyncs only the files written since their last sync, and recovery fsyncs before it drops the WAL; a second embedded instance’s clean close could delete another live instance’s files (<a href="https://github.com/ArcadeData/arcadedb/issues/8626">#8626</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7479">#7479</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7502">#7502</a>); and a WAL housekeeping timer could delete recovery input (<a href="https://github.com/ArcadeData/arcadedb/pull/8600">#8600</a>).</li>
  <li><strong>The schema is written once per DDL statement</strong>, never mid-transaction, and <code class="language-plaintext highlighter-rouge">schema.json</code> is replaced atomically (<a href="https://github.com/ArcadeData/arcadedb/issues/8635">#8635</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/7279">#7279</a>).</li>
</ul>

<h3 id="the-same-value-now-compares-equal-everywhere">The Same Value Now Compares Equal Everywhere</h3>

<p>The index, a SQL scan and Cypher disagreed on what “equal” means for several types, so the same <code class="language-plaintext highlighter-rouge">WHERE</code> returned different rows depending on the plan. They agree now for <code class="language-plaintext highlighter-rouge">FLOAT</code>/<code class="language-plaintext highlighter-rouge">DOUBLE</code>, <code class="language-plaintext highlighter-rouge">DECIMAL</code>, <code class="language-plaintext highlighter-rouge">DATETIME</code>, <code class="language-plaintext highlighter-rouge">BOOLEAN</code>, <code class="language-plaintext highlighter-rouge">BigInteger</code>, signed zero and null (<a href="https://github.com/ArcadeData/arcadedb/issues/8884">#8884</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8885">#8885</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8886">#8886</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8887">#8887</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9113">#9113</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9114">#9114</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9160">#9160</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9274">#9274</a>).</p>

<ul>
  <li><strong>Null equality is decided at execution, not at planning.</strong> A statement first run with a value kept its index plan, so a later run with <code class="language-plaintext highlighter-rouge">null</code> returned the null-key records again, and a <code class="language-plaintext highlighter-rouge">DELETE</code> could remove the wrong rows (<a href="https://github.com/ArcadeData/arcadedb/issues/9274">#9274</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9238">#9238</a>). <code class="language-plaintext highlighter-rouge">UPDATE</code>/<code class="language-plaintext highlighter-rouge">DELETE</code> with positional parameters read the <code class="language-plaintext highlighter-rouge">WHERE</code> from an earlier cached statement (<a href="https://github.com/ArcadeData/arcadedb/issues/9245">#9245</a>).</li>
  <li><strong>Precision.</strong> A SQL decimal literal keeps digits beyond double precision, JSONL import keeps <code class="language-plaintext highlighter-rouge">DECIMAL</code> precision, a suffix-less floating point literal is a <code class="language-plaintext highlighter-rouge">Double</code>, and numeric comparison past 2^53 is exact (<a href="https://github.com/ArcadeData/arcadedb/issues/8872">#8872</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8871">#8871</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7609">#7609</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7628">#7628</a>).</li>
  <li><strong>Datetimes.</strong> An unparseable datetime is refused instead of stored as <code class="language-plaintext highlighter-rouge">NULL</code>, and <code class="language-plaintext highlighter-rouge">DATETIME_MICROS</code> literals with a space separator stop being truncated (<a href="https://github.com/ArcadeData/arcadedb/issues/8090">#8090</a>); sub-millisecond precision and temporal round trips were fixed in Cypher and over the wire.</li>
  <li><strong>Aggregates.</strong> <code class="language-plaintext highlighter-rouge">LONG</code> sum overflow, <code class="language-plaintext highlighter-rouge">COUNT(DISTINCT expr)</code> (a syntax error until now), aggregates in <code class="language-plaintext highlighter-rouge">ORDER BY</code>, and <code class="language-plaintext highlighter-rouge">min()</code>/<code class="language-plaintext highlighter-rouge">max()</code> over an index range (<a href="https://github.com/ArcadeData/arcadedb/issues/8972">#8972</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8889">#8889</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8973">#8973</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8812">#8812</a>).</li>
</ul>

<blockquote>
  <p>A result set that changes after the upgrade is the fix, not a regression.</p>
</blockquote>

<h3 id="faster-queries">Faster Queries</h3>

<ul>
  <li><strong>Parallel scans, aggregation and <code class="language-plaintext highlighter-rouge">DISTINCT</code>.</strong> Filtered scans and aggregation run on the dedicated scan workers, a transaction that has written nothing still scans in parallel, <code class="language-plaintext highlighter-rouge">POST /query</code> no longer opens an auto-commit transaction that blocked it, and a satisfied <code class="language-plaintext highlighter-rouge">LIMIT</code> releases its upstream (<a href="https://github.com/ArcadeData/arcadedb/issues/8523">#8523</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8775">#8775</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8594">#8594</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8799">#8799</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8797">#8797</a>). Scans also stopped re-resolving each record and build records from the page in hand (<a href="https://github.com/ArcadeData/arcadedb/issues/8265">#8265</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8266">#8266</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8312">#8312</a>).</li>
  <li><strong>The index serves more shapes</strong>: Cypher <code class="language-plaintext highlighter-rouge">ORDER BY ... LIMIT</code> from index order, <code class="language-plaintext highlighter-rouge">LIKE 'prefix%'</code>, <code class="language-plaintext highlighter-rouge">STARTS WITH</code>, Cypher <code class="language-plaintext highlighter-rouge">min()</code>/<code class="language-plaintext highlighter-rouge">max()</code>, Cypher <code class="language-plaintext highlighter-rouge">OR</code> index seeks, case-insensitive index ranges, and an adaptive range fetch that parallelises large ranges (<a href="https://github.com/ArcadeData/arcadedb/issues/8422">#8422</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8666">#8666</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8723">#8723</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8766">#8766</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8333">#8333</a>).</li>
  <li><strong>Concurrent RID lookups scale.</strong> The database read/write lock is striped (<a href="https://github.com/ArcadeData/arcadedb/issues/8838">#8838</a>), and <code class="language-plaintext highlighter-rouge">checkDatabaseIsOpen</code> no longer writes <code class="language-plaintext highlighter-rouge">lastUsedOn</code> on every call (<a href="https://github.com/ArcadeData/arcadedb/issues/8523">#8523</a>).</li>
  <li><strong>Plan caching.</strong> <code class="language-plaintext highlighter-rouge">UPDATE</code>, <code class="language-plaintext highlighter-rouge">DELETE</code> and <code class="language-plaintext highlighter-rouge">INSERT</code> no longer re-plan on every execution (<a href="https://github.com/ArcadeData/arcadedb/issues/9207">#9207</a>), and a correlated LET subquery is memoized per outer binding (<a href="https://github.com/ArcadeData/arcadedb/issues/8400">#8400</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8441">#8441</a>).</li>
  <li><strong>Graph algorithms.</strong> <code class="language-plaintext highlighter-rouge">algo.wcc</code> uses a parallel union-find (Afforest) (<a href="https://github.com/ArcadeData/arcadedb/issues/9133">#9133</a>), and <code class="language-plaintext highlighter-rouge">algo.*</code> waits for the restored Graph Analytical View on the first call after a reopen (<a href="https://github.com/ArcadeData/arcadedb/issues/9220">#9220</a>).</li>
  <li><strong>Cypher joins on disconnected patterns</strong> use a value hash join with compact, bounded buffers instead of buffering the full record of every right-hand row (<a href="https://github.com/ArcadeData/arcadedb/issues/8583">#8583</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8584">#8584</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8585">#8585</a>).</li>
  <li><strong>A JVM-wide heap budget for query buffers</strong> keeps concurrent SQL and OpenCypher queries from exhausting the heap together (<a href="https://github.com/ArcadeData/arcadedb/issues/8591">#8591</a>).</li>
  <li><strong>TimeSeries</strong>: tag-skipping mutable scans, merging of small sealed blocks, shared tag strings and bucket origin/offset for weekly buckets (<a href="https://github.com/ArcadeData/arcadedb/issues/8574">#8574</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8798">#8798</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8794">#8794</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8793">#8793</a>). For the numbers behind ArcadeDB’s engine speed, see the <a href="https://arcadedb.com/benchmarks.html">benchmarks</a>.</li>
</ul>

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

<p>Teams building retrieval pipelines on the <a href="https://arcadedb.com/graph-rag.html">GraphRAG stack</a> get a round of correctness fixes in <code class="language-plaintext highlighter-rouge">LSM_VECTOR</code> and <code class="language-plaintext highlighter-rouge">LSM_SPARSE_VECTOR</code>:</p>

<ul>
  <li><strong>The graph rebuild loop stops when two records share a vector id</strong>, and <code class="language-plaintext highlighter-rouge">CHECK DATABASE FIX</code> repairs it (<a href="https://github.com/ArcadeData/arcadedb/pull/8946">#8946</a>).</li>
  <li><strong>Aborted transactions no longer leak</strong> tombstones or uncommitted vectors into vector indexes, a rebuild reads committed state only, and a delete reuses the persisted graph (<a href="https://github.com/ArcadeData/arcadedb/issues/7931">#7931</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7974">#7974</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7842">#7842</a>).</li>
  <li><strong>Searches during an async rebuild returned poor neighbors</strong>; the first search after a reopen loads the graph via the ordinal map; a query that rebuilds a graph honours the JVM-wide rebuild limit (<a href="https://github.com/ArcadeData/arcadedb/issues/8862">#8862</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8852">#8852</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7814">#7814</a>).</li>
  <li><strong>A <code class="language-plaintext highlighter-rouge">filter</code> that matches no records</strong> was treated as no filter and returned records outside it (<a href="https://github.com/ArcadeData/arcadedb/issues/8959">#8959</a>); grouped search fills every winning group (<a href="https://github.com/ArcadeData/arcadedb/issues/8002">#8002</a>).</li>
  <li><strong>Compaction lost commits</strong> and a search during compaction was wrong; <code class="language-plaintext highlighter-rouge">BINARY</code> quantization, zero and non-finite vectors, and a window-based MaxScore for SPLADE top-K were fixed (<a href="https://github.com/ArcadeData/arcadedb/pull/9132">#9132</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9252">#9252</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9200">#9200</a>).</li>
  <li><strong>The HNSW build cache is sized from the heap ceiling</strong>, so a served database matches an embedded one (<a href="https://github.com/ArcadeData/arcadedb/issues/7146">#7146</a>).</li>
</ul>

<p>New: <code class="language-plaintext highlighter-rouge">vector.neighbors</code> accepts a subquery result as its filter, dense vector search sees the open transaction’s rows, and sparse-vector indexes can be built over existing rows and rank by exact score (<a href="https://github.com/ArcadeData/arcadedb/issues/7125">#7125</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7378">#7378</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8536">#8536</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8576">#8576</a>).</p>

<h3 id="wire-protocols-tls-for-postgres-and-100-fidelity-fixes">Wire Protocols: TLS for Postgres and 100+ Fidelity Fixes</h3>

<ul>
  <li><strong>TLS/SSL for the Postgres wire protocol</strong> (<a href="https://github.com/ArcadeData/arcadedb/issues/8840">#8840</a>), plus <code class="language-plaintext highlighter-rouge">COPY (&lt;query&gt;) TO STDOUT</code> in text and binary format (<a href="https://github.com/ArcadeData/arcadedb/issues/7188">#7188</a>).</li>
  <li><strong>Postgres</strong>: <code class="language-plaintext highlighter-rouge">SET</code> is session-scoped, transactional and read back by <code class="language-plaintext highlighter-rouge">SHOW</code>; <code class="language-plaintext highlighter-rouge">COMMIT</code>/<code class="language-plaintext highlighter-rouge">ROLLBACK</code> inside an implicit transaction block were silently ignored; a trailing semicolon bypassed the transaction-block state machine; <code class="language-plaintext highlighter-rouge">ROLLBACK TO SAVEPOINT</code> is refused instead of accepted as a no-op; portals are transaction-scoped; <code class="language-plaintext highlighter-rouge">$N</code> parameters work on indexes; the Arrow ADBC driver bootstraps (<code class="language-plaintext highlighter-rouge">pg_type</code>, <code class="language-plaintext highlighter-rouge">regclass</code>); and binary arrays work (<a href="https://github.com/ArcadeData/arcadedb/issues/8028">#8028</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8245">#8245</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8217">#8217</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7178">#7178</a>).</li>
  <li><strong>MongoDB</strong>: lossless BSON types (Binary, regex, timestamp, MinKey, MaxKey and JavaScript were silently dropped), exact filter semantics, nested <code class="language-plaintext highlighter-rouge">$set</code>, projection, unique <code class="language-plaintext highlighter-rouge">_id</code> and replace (<a href="https://github.com/ArcadeData/arcadedb/issues/9062">#9062</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9137">#9137</a>).</li>
  <li><strong>Redis</strong>: a newline batch was classified by its first command only, so a <code class="language-plaintext highlighter-rouge">GET</code> followed by an <code class="language-plaintext highlighter-rouge">HDEL</code> was declared read-only; <code class="language-plaintext highlighter-rouge">INCR</code>/<code class="language-plaintext highlighter-rouge">DECR</code> are atomic on the query engine too; <code class="language-plaintext highlighter-rouge">MULTI</code>/<code class="language-plaintext highlighter-rouge">EXEC</code> no longer double-applies; values are binary-safe; <code class="language-plaintext highlighter-rouge">INCRBYFLOAT</code> is supported (<a href="https://github.com/ArcadeData/arcadedb/issues/8247">#8247</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8248">#8248</a>).</li>
  <li><strong>Bolt</strong>: node ids match <code class="language-plaintext highlighter-rouge">id(n)</code>, system procedures are served only when the statement is that call, and failures are named.</li>
  <li><strong>gRPC and remote clients</strong>: a lost response on a self-committing write is an unknown outcome, not a retryable error; <code class="language-plaintext highlighter-rouge">RemoteDatabase.transaction()</code> has the partial-commit guard; <code class="language-plaintext highlighter-rouge">RemoteServer</code> drop/createUser/dropUser go through <code class="language-plaintext highlighter-rouge">httpCommand</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/8525">#8525</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8062">#8062</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7796">#7796</a>).</li>
  <li><strong>Gremlin</strong>: label loss, profile double execution, idempotent <code class="language-plaintext highlighter-rouge">drop</code>, fractional index bounds and the advertised Gremlin port were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/8258">#8258</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7408">#7408</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9147">#9147</a>).</li>
</ul>

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

<p>This release closes <strong>four security advisories</strong>, published in full (impact, affected versions and credit) as <a href="https://github.com/ArcadeData/arcadedb/security/advisories">GitHub Security Advisories</a> on the repository. All four affect <strong>26.9.1 and earlier</strong>, are patched in 26.10.1, and were reported by <a href="https://github.com/manus-pi">@manus-pi</a>.</p>

<ul>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-h2j4-28h8-cj5v">GHSA-h2j4-28h8-cj5v</a></strong> (critical): the Gremlin Groovy engine lacked a scripting authorization check, enabling remote code execution by an authenticated user.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-7fcg-pg8x-wf7w">GHSA-7fcg-pg8x-wf7w</a></strong> (medium): Bolt <code class="language-plaintext highlighter-rouge">SHOW DATABASES</code> listed every database on the server without per-user authorization filtering.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-hw9x-xx38-rg28">GHSA-hw9x-xx38-rg28</a></strong> (medium): Bolt lacked per-database authorization, so an authenticated user could read another database’s schema metadata.</li>
  <li><strong><a href="https://github.com/ArcadeData/arcadedb/security/advisories/GHSA-9c7g-grf7-j2r5">GHSA-9c7g-grf7-j2r5</a></strong> (medium): read-only users could change edge type <code class="language-plaintext highlighter-rouge">lightweight</code>/<code class="language-plaintext highlighter-rouge">unique</code> schema flags via <code class="language-plaintext highlighter-rouge">ALTER TYPE</code>, bypassing <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code>.</li>
</ul>

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

<ul>
  <li><strong>Bolt</strong> applies the per-user database and type access to database selection and schema listings (<a href="https://github.com/ArcadeData/arcadedb/pull/8415">#8415</a>).</li>
  <li><strong>Schema DDL gates</strong>: <code class="language-plaintext highlighter-rouge">ALTER TYPE</code> edge-type settings and <code class="language-plaintext highlighter-rouge">REBUILD TYPE</code> require <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code> (<a href="https://github.com/ArcadeData/arcadedb/pull/8398">#8398</a>).</li>
  <li><strong>Gremlin scripting</strong> is restricted further, including <code class="language-plaintext highlighter-rouge">io()</code> and lambdas (<a href="https://github.com/ArcadeData/arcadedb/pull/8227">#8227</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/8338">#8338</a>).</li>
  <li><strong>Request bounds</strong>: a chunked request body no longer bypasses <code class="language-plaintext highlighter-rouge">httpBodyContentMaxSize</code>, gzip and Snappy ingest are bounded after decompression, and <code class="language-plaintext highlighter-rouge">/ws</code> frames are bounded (<a href="https://github.com/ArcadeData/arcadedb/issues/7772">#7772</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8084">#8084</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8065">#8065</a>). MongoDB wire regexes in aggregation are time-bounded (<a href="https://github.com/ArcadeData/arcadedb/issues/9164">#9164</a>), and a TimeSeries aggregation checks its caller’s ceiling before allocating buckets (<a href="https://github.com/ArcadeData/arcadedb/issues/7476">#7476</a>).</li>
  <li><strong>Secrets</strong>: <code class="language-plaintext highlighter-rouge">set_server_setting</code> and the MCP request log mask hidden settings, and <code class="language-plaintext highlighter-rouge">resetAll()</code> no longer dumps the whole configuration (<a href="https://github.com/ArcadeData/arcadedb/issues/8038">#8038</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/8965">#8965</a>).</li>
  <li><strong>AI chat store</strong>: the legacy-directory migration no longer awards a collided chat store to the wrong user, and accounts that differ only in case no longer share one (<a href="https://github.com/ArcadeData/arcadedb/issues/7620">#7620</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8154">#8154</a>).</li>
  <li><strong>HA</strong>: five TLS and security issues were fixed, security mutations are refused off the leader, and the Raft gRPC peer allow-list is no longer frozen at startup (<a href="https://github.com/ArcadeData/arcadedb/pull/7854">#7854</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/8407">#8407</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/7162">#7162</a>).</li>
  <li><strong>HTTP Basic</strong> credentials are split on the first colon only (<a href="https://github.com/ArcadeData/arcadedb/issues/7783">#7783</a>).</li>
</ul>

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

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

<ul>
  <li>A write to a record the transaction already deleted is dropped, not reported as a deadlock (<a href="https://github.com/ArcadeData/arcadedb/issues/7149">#7149</a>).</li>
  <li>Bucket space reuse, a free tail measured directly on a rebased delete, and edge delete page merges were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/8660">#8660</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8401">#8401</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9233">#9233</a>).</li>
  <li>LIGHTWEIGHT edge types: reads answered 0, and <code class="language-plaintext highlighter-rouge">TRUNCATE</code> and <code class="language-plaintext highlighter-rouge">TRAVERSE</code> were wrong (<a href="https://github.com/ArcadeData/arcadedb/issues/7477">#7477</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7480">#7480</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7481">#7481</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">CHECK DATABASE</code> finds records that violate their own type’s existence constraints (<a href="https://github.com/ArcadeData/arcadedb/issues/7952">#7952</a>), and a failed <code class="language-plaintext highlighter-rouge">REBUILD INDEX</code> restores the old index (<a href="https://github.com/ArcadeData/arcadedb/issues/9254">#9254</a>).</li>
  <li>A <code class="language-plaintext highlighter-rouge">LIMIT</code> query could leave the database unable to close because cancelling its parallel scan interrupted a page read (<a href="https://github.com/ArcadeData/arcadedb/issues/8944">#8944</a>).</li>
  <li>Windows backup archive nesting and path separators were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/7586">#7586</a>), and the ext4 <code class="language-plaintext highlighter-rouge">lost+found</code> directory is no longer registered as a database (<a href="https://github.com/ArcadeData/arcadedb/issues/8805">#8805</a>).</li>
  <li>Backups cover the configuration files in the snapshot (<a href="https://github.com/ArcadeData/arcadedb/issues/6114">#6114</a>), and the HA snapshot ZIP skips index-compaction temporaries (<a href="https://github.com/ArcadeData/arcadedb/issues/8019">#8019</a>).</li>
</ul>

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

<ul>
  <li>A range seek landed in the middle of a same-key run (<a href="https://github.com/ArcadeData/arcadedb/issues/7611">#7611</a>); transaction index ranges after compaction and the tx-overlay filtering rules shared across read paths were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/8817">#8817</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/6970">#6970</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">CREATE INDEX</code> on a populated type no longer truncates datetimes to milliseconds, and <code class="language-plaintext highlighter-rouge">CREATE INDEX IF NOT EXISTS</code> works across super types (<a href="https://github.com/ArcadeData/arcadedb/issues/7164">#7164</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7228">#7228</a>).</li>
  <li>SQL queries no longer fail while an index is created or dropped on the same type (<a href="https://github.com/ArcadeData/arcadedb/issues/8855">#8855</a>).</li>
  <li>Hash index overflow walk cost, prefix seek and lookup at page end were fixed, along with unique prefix lookup and <code class="language-plaintext highlighter-rouge">IS NULL</code> on unique indexes (<a href="https://github.com/ArcadeData/arcadedb/issues/9253">#9253</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8806">#8806</a>).</li>
  <li>A unique <code class="language-plaintext highlighter-rouge">BINARY</code> key skipped the uniqueness check because array keys were compared shallowly (<a href="https://github.com/ArcadeData/arcadedb/issues/7881">#7881</a>).</li>
</ul>

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

<ul>
  <li>Parser re-render, <code class="language-plaintext highlighter-rouge">copy()</code>, <code class="language-plaintext highlighter-rouge">LIMIT</code> counting, positional <code class="language-plaintext highlighter-rouge">LIMIT ? SKIP ?</code> binding, comments in <code class="language-plaintext highlighter-rouge">ORDER BY</code>/<code class="language-plaintext highlighter-rouge">GROUP BY</code>, hex literals, nesting guards and planner exceptions were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/7800">#7800</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8434">#8434</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9122">#9122</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/9154">#9154</a>).</li>
  <li>A scalar left operand of <code class="language-plaintext highlighter-rouge">CONTAINSANY</code> silently matched nothing, and <code class="language-plaintext highlighter-rouge">expand()</code> of a native array yields one row per element (<a href="https://github.com/ArcadeData/arcadedb/pull/8475">#8475</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7910">#7910</a>).</li>
  <li>The <code class="language-plaintext highlighter-rouge">UPDATE</code> Halloween problem, <code class="language-plaintext highlighter-rouge">GROUP BY</code> projection alias rebuilt per record, <code class="language-plaintext highlighter-rouge">ORDER BY</code> over a null scan, and <code class="language-plaintext highlighter-rouge">FULL_TEXT</code> never answering exact lookups were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/8814">#8814</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8260">#8260</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8664">#8664</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8442">#8442</a>).</li>
  <li>SQL scripts replay blocks correctly, and <code class="language-plaintext highlighter-rouge">EXPLAIN</code> has parity across sqlscript operation types (<a href="https://github.com/ArcadeData/arcadedb/issues/8633">#8633</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7576">#7576</a>).</li>
</ul>

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

<ul>
  <li>Relationship uniqueness is scoped to the clause, <code class="language-plaintext highlighter-rouge">MERGE</code> binds the path it merged, and an eager read/write barrier means a query never reads back what it just created (<a href="https://github.com/ArcadeData/arcadedb/issues/7165">#7165</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7169">#7169</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7171">#7171</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">MERGE ... ON MATCH SET</code> no longer loses a concurrent increment, nodes are written once for <code class="language-plaintext highlighter-rouge">MERGE ... SET</code> and <code class="language-plaintext highlighter-rouge">CREATE ... SET</code>, and <code class="language-plaintext highlighter-rouge">LIMIT</code> after a write no longer cuts the write short (<a href="https://github.com/ArcadeData/arcadedb/issues/8538">#8538</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8735">#8735</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8826">#8826</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8827">#8827</a>).</li>
  <li>An empty <code class="language-plaintext highlighter-rouge">FOREACH</code> changed <code class="language-plaintext highlighter-rouge">OPTIONAL MATCH</code> results; <code class="language-plaintext highlighter-rouge">FOREACH</code>/<code class="language-plaintext highlighter-rouge">REMOVE</code> scope checks, existence constraints checked at end of statement, and <code class="language-plaintext highlighter-rouge">CALL (*)</code> no longer inheriting the outer clauses’ variables were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/8733">#8733</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8105">#8105</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9205">#9205</a>).</li>
  <li>Unidirectional <code class="language-plaintext highlighter-rouge">in()</code>/<code class="language-plaintext highlighter-rouge">both()</code> with a view, queries on the incoming side of unidirectional edge types, <code class="language-plaintext highlighter-rouge">DELETE</code> of a node with several relationships, and code-point string handling were fixed (<a href="https://github.com/ArcadeData/arcadedb/issues/8939">#8939</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8625">#8625</a>).</li>
</ul>

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

<ul>
  <li>A <code class="language-plaintext highlighter-rouge">TIMESTAMP</code> column not declared first round-tripped through JSONL with its values in the wrong columns (<a href="https://github.com/ArcadeData/arcadedb/issues/7899">#7899</a>), and aggregation push-down mixed row and schema column indexes between the mutable and sealed halves (<a href="https://github.com/ArcadeData/arcadedb/issues/8140">#8140</a>).</li>
  <li>A query now gives one answer whichever layer holds the data; sealed-store identity and repair were fixed, and an empty <code class="language-plaintext highlighter-rouge">TAG</code> key is refused instead of dropped silently (<a href="https://github.com/ArcadeData/arcadedb/issues/7733">#7733</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8738">#8738</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8563">#8563</a>).</li>
  <li>PromQL conformance gaps were closed, and <code class="language-plaintext highlighter-rouge">ts.timeBucket()</code> is exposed as <code class="language-plaintext highlighter-rouge">LocalDateTime</code> (<a href="https://github.com/ArcadeData/arcadedb/issues/8926">#8926</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7610">#7610</a>).</li>
</ul>

<h3 id="importer-graphbatch-and-async">Importer, GraphBatch and Async</h3>

<ul>
  <li>Importer transaction ownership, RDF and CSV edge cases, row-error handling and post-import validation were fixed (<a href="https://github.com/ArcadeData/arcadedb/pull/7943">#7943</a>, <a href="https://github.com/ArcadeData/arcadedb/pull/7948">#7948</a>).</li>
  <li><code class="language-plaintext highlighter-rouge">GraphBatch</code> wrote the header end offset one byte short on property-less edges, wrote a null for a declared edge property as a type tag with no value, and now honours the caller’s transaction (<a href="https://github.com/ArcadeData/arcadedb/issues/7448">#7448</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9018">#9018</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9242">#9242</a>).</li>
  <li>A periodic batch-commit conflict is retried instead of silently discarded (<a href="https://github.com/ArcadeData/arcadedb/issues/7615">#7615</a>), and <code class="language-plaintext highlighter-rouge">insert_many</code> and <code class="language-plaintext highlighter-rouge">GraphBatch.create_vertex</code> roll back on every failure path (<a href="https://github.com/ArcadeData/arcadedb/issues/7882">#7882</a>).</li>
</ul>

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

<p>Behaviour changes are collected in the <a href="https://github.com/ArcadeData/arcadedb/releases/tag/26.10.1">release notes</a>. The ones worth checking before you upgrade:</p>

<ol>
  <li><strong>Security-convergence readiness gate is on by default</strong> on HA clusters, armed per join. A joining node is not reported ready until its security state matches the leader’s (<a href="https://github.com/ArcadeData/arcadedb/issues/7819">#7819</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8414">#8414</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">arcadedb.server.apiTokenRequireSecureTransport</code></strong>: check the first-run Docker flow on <code class="language-plaintext highlighter-rouge">http://localhost:2480</code>, where Studio’s API-token mint is refused once it is on (<a href="https://github.com/ArcadeData/arcadedb/issues/8765">#8765</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7804">#7804</a>).</li>
  <li><strong>Stricter refusals instead of silent acceptance</strong>: an unparseable datetime, a map value the declared type cannot take, <code class="language-plaintext highlighter-rouge">Type.convert</code> of an impossible value, <code class="language-plaintext highlighter-rouge">ROLLBACK TO SAVEPOINT</code> over the Postgres wire, an <code class="language-plaintext highlighter-rouge">UPDATE</code> whose value was computed from a stale read, and <code class="language-plaintext highlighter-rouge">TRUNCATE TYPE</code> on a non-empty vertex or edge type (<a href="https://github.com/ArcadeData/arcadedb/issues/8090">#8090</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/9110">#9110</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8610">#8610</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/8042">#8042</a>).</li>
  <li><strong>Queries that returned a plan-dependent answer now return the equality-consistent one</strong> (null keys, <code class="language-plaintext highlighter-rouge">FLOAT</code>/<code class="language-plaintext highlighter-rouge">DECIMAL</code>/<code class="language-plaintext highlighter-rouge">DATETIME</code>/<code class="language-plaintext highlighter-rouge">BOOLEAN</code>, signed zero).</li>
  <li><strong>Index default page size</strong> changed (<a href="https://github.com/ArcadeData/arcadedb/pull/9250">#9250</a>); existing indexes keep their size.</li>
  <li><strong>Schema DDL gates</strong>: <code class="language-plaintext highlighter-rouge">ALTER TYPE</code> edge-type settings and <code class="language-plaintext highlighter-rouge">REBUILD TYPE</code> need <code class="language-plaintext highlighter-rouge">UPDATE_SCHEMA</code> (<a href="https://github.com/ArcadeData/arcadedb/pull/8398">#8398</a>); Gremlin scripting and <code class="language-plaintext highlighter-rouge">io()</code> are restricted (<a href="https://github.com/ArcadeData/arcadedb/pull/8227">#8227</a>).</li>
  <li><strong>Rolling upgrade of an HA cluster</strong>: peers negotiate capabilities, and group or API-token changes wait until every peer supports them; Studio’s cluster page shows the state (<a href="https://github.com/ArcadeData/arcadedb/issues/7219">#7219</a>, <a href="https://github.com/ArcadeData/arcadedb/issues/7538">#7538</a>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">LSM_SPARSE_VECTOR</code> rescoring</strong> oversamples by default (<code class="language-plaintext highlighter-rouge">rescoreOversample</code> 2) and ranks by exact score (<a href="https://github.com/ArcadeData/arcadedb/issues/8576">#8576</a>).</li>
</ol>

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

<p>About 140 dependency bumps landed in this cycle, almost all through Dependabot. The notable ones: Netty <strong>4.2.18</strong>, Apache Ratis <strong>3.3.1</strong>, Gremlin (TinkerPop) <strong>3.8.2</strong>, GraalVM <strong>25.x</strong>, JVector <strong>4.0.1</strong>, Jackson <strong>2.22.3</strong>, Protobuf <strong>4.36.2</strong>, SLF4J <strong>2.0.20</strong>, Logback <strong>1.6.5</strong>, OpenTelemetry <strong>1.66.0</strong>, SnakeYAML <strong>2.7</strong>, lz4-java <strong>1.12.0</strong>, commons-lang3 <strong>3.21.0</strong>, JLine <strong>4.4.6</strong>, Neo4j Java driver <strong>6.3.0</strong>, Jedis <strong>8.0.1</strong> and gRPC protos <strong>2.78.0</strong>. On the Studio side: ApexCharts <strong>7.6.1</strong>, marked <strong>18.0.14</strong> and swagger-ui-dist <strong>5.33.0</strong>.</p>

<h2 id="getting-started-with-26101">Getting Started with 26.10.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.10.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.10.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>

<hr />

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

<p>Thanks to everyone who reported, reproduced, reviewed, tested and fixed, in particular <a href="https://github.com/ruispereira">@ruispereira</a>, <a href="https://github.com/tae898">@tae898</a>, <a href="https://github.com/rspereiratech">@rspereiratech</a>, <a href="https://github.com/YGY-001">@YGY-001</a>, <a href="https://github.com/jjj-n">@jjj-n</a>, <a href="https://github.com/gramian">@gramian</a>, <a href="https://github.com/Das-Rabindra">@Das-Rabindra</a>, <a href="https://github.com/singhpratech">@singhpratech</a>, <a href="https://github.com/rlaveycal">@rlaveycal</a>, <a href="https://github.com/wkpark">@wkpark</a>, <a href="https://github.com/rbonestell">@rbonestell</a>, <a href="https://github.com/justinblethrow-cloud">@justinblethrow-cloud</a>, <a href="https://github.com/philiptran-tech">@philiptran-tech</a>, <a href="https://github.com/GYWang1983">@GYWang1983</a>, <a href="https://github.com/astarso">@astarso</a>, <a href="https://github.com/mdre">@mdre</a>, <a href="https://github.com/ivan-velikanov">@ivan-velikanov</a>, <a href="https://github.com/willemijn-zeno">@willemijn-zeno</a>, <a href="https://github.com/shlomiassaf">@shlomiassaf</a>, <a href="https://github.com/nx-mama">@nx-mama</a>, <a href="https://github.com/maurovit">@maurovit</a>, <a href="https://github.com/LetMeSleep8h">@LetMeSleep8h</a>, <a href="https://github.com/suhanrain">@suhanrain</a>, <a href="https://github.com/manus-pi">@manus-pi</a>, <a href="https://github.com/BingEdward">@BingEdward</a>, <a href="https://github.com/anatshad">@anatshad</a>, <a href="https://github.com/borutjures">@borutjures</a>, <a href="https://github.com/ExtReMLapin">@ExtReMLapin</a>, <a href="https://github.com/jawahar4k">@jawahar4k</a>, <a href="https://github.com/JMQuill-SF">@JMQuill-SF</a>, <a href="https://github.com/M-Tesla">@M-Tesla</a>, <a href="https://github.com/odysseaspenta">@odysseaspenta</a>, <a href="https://github.com/sainiteesh36">@sainiteesh36</a>, <a href="https://github.com/shulei5831sl">@shulei5831sl</a>, <a href="https://github.com/singh-hovr">@singh-hovr</a> and <a href="https://github.com/wsalembi">@wsalembi</a>.</p>

<p>Luca Garulli
ArcadeDB Founder</p>]]></content><author><name>Luca Garulli</name></author><category term="Multi-Model" /><category term="High Availability" /><category term="Raft" /><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.10.1 closes 1,615 issues and pull requests across about 3,100 commits. Raft high availability is hardened end to end, lost updates and silent data loss are closed, index, SQL and Cypher now agree on equality, queries run faster in parallel, the Postgres wire protocol gains TLS, and four security advisories are patched.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/release-v26.10.1.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/release-v26.10.1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB vs PostgreSQL for Graph Queries: Why SQL/PGQ Doesn’t Replace a Graph Database</title><link href="https://arcadedb.com/blog/arcadedb-vs-postgresql-graph-queries-sql-pgq/" rel="alternate" type="text/html" title="ArcadeDB vs PostgreSQL for Graph Queries: Why SQL/PGQ Doesn’t Replace a Graph Database" /><published>2026-09-28T00:00:00+00:00</published><updated>2026-09-28T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-vs-postgresql-graph-queries-sql-pgq</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-vs-postgresql-graph-queries-sql-pgq/"><![CDATA[<p>Let’s start with the credit PostgreSQL deserves. It is one of the best relational databases ever built, and arguably the most important open-source DBMS of the last thirty years: community-governed, permissively licensed, famously careful about correctness, and extensible enough to carry PostGIS, pgvector, and a whole ecosystem on top. Its wire protocol has become a lingua franca that many other databases, ArcadeDB included, speak so they can reuse its drivers and tools. If you need a relational database, PostgreSQL is very hard to beat.</p>

<p>But when the use case is a real graph, it is not the best choice. Not even close.</p>

<p>That needs saying now, because when the PostgreSQL 19 betas shipped <a href="https://www.kynodex.com/blog/postgresql-19-is-a-graph-database-now-and-you-dont-need-neo4j">SQL/PGQ</a>, the ISO SQL:2023 standard for property graph queries, one conclusion spread fast: <strong>PostgreSQL is a graph database now</strong>. “You don’t need Neo4j.” “<a href="https://medium.com/@ArkProtocol1/postgres-19-adds-graph-queries-i-ran-our-neo4j-workload-against-it-41aab1bc82c5">I ran our Neo4j workload against it</a>.”</p>

<p>Then, on September 7, 2026, <a href="https://neon.com/postgresql/postgresql-19-new-features#sqlpgq-property-graph-queries">the feature was reverted from PostgreSQL 19</a> over stability concerns. The earliest it can return is PostgreSQL 20, in September 2027.</p>

<p>The delay is not the interesting part. SQL/PGQ will land eventually, and the real question is: <strong>once it does, can PostgreSQL replace a graph database for a real graph workload?</strong> We ran the LDBC LSQB benchmark to find out. <strong>ArcadeDB finished all nine queries in 2.2 seconds. PostgreSQL needed 69.</strong></p>

<h2 id="what-sqlpgq-in-postgresql-actually-is">What SQL/PGQ in PostgreSQL Actually Is</h2>

<p>SQL/PGQ lets you declare a property graph over tables you already have, and query it with a pattern instead of a stack of joins. That is a genuine readability win. But the <a href="https://exobench.ai/blog/pg19-graph-queries-part-1">exobench analysis of the beta</a> is clear about what it is: “deliberately not a graph storage engine. It is a rewriter.” Every <code class="language-plaintext highlighter-rouge">MATCH</code> compiles into the joins you would have written by hand, with <strong>identical execution plans</strong>.</p>

<p><img style="display:block;margin:32px auto" src="/assets/images/arcadedb-vs-postgresql-sqlpgq-rewriter.svg" alt="A GRAPH_TABLE query goes into the SQL/PGQ rewriter and comes out as a SELECT with four JOINs" width="1000" height="330" /></p>

<p>The beta also left out most of what graph work needs:</p>

<ul>
  <li><strong>No variable-length paths</strong>: quantifiers like <code class="language-plaintext highlighter-rouge">{1,5}</code> were rejected.</li>
  <li><strong>Read-only</strong>: writes go to the underlying tables, in plain SQL.</li>
  <li><strong>No graph algorithms</strong>: no PageRank, no community detection.</li>
  <li><strong>Schema-bound</strong>: every edge type is a table, declared up front.</li>
</ul>

<h2 id="syntax-is-not-storage-joins-vs-index-free-adjacency">Syntax Is Not Storage: Joins vs Index-Free Adjacency</h2>

<p>In PostgreSQL, an edge is a row, so every hop is a B-tree search that gets slower as the table grows. In ArcadeDB, each vertex holds direct links (Record IDs) to its edges, so a hop is a pointer jump whose cost does not depend on the size of the database. That is <strong>index-free adjacency</strong>. The difference is invisible at one hop and dominant at three.</p>

<p><img style="display:block;margin:32px auto" src="/assets/images/arcadedb-vs-postgresql-hops.svg" alt="Animation: PostgreSQL runs one index search per hop, A to B, B to C, C to D, and its query clock reaches 200 ms; ArcadeDB follows three direct links and finishes in 20 ms on the same clock" width="1000" height="550" /></p>

<h2 id="the-benchmark-ldbc-lsqb-arcadedb-vs-postgresql">The Benchmark: LDBC LSQB, ArcadeDB vs PostgreSQL</h2>

<p><a href="https://github.com/ldbc/lsqb">LSQB</a> is maintained by the Linked Data Benchmark Council, not by us: nine pattern-matching queries, with an official SQL version for relational systems and an official Cypher version for graph systems. We ran it at scale factor 1 (3.9 million vertices, 17.9 million edges). Since <code class="language-plaintext highlighter-rouge">GRAPH_TABLE</code> compiles to the same plan as those SQL joins, <strong>PostgreSQL’s numbers are exactly what SQL/PGQ would get</strong>.</p>

<p><img style="display:block;margin:32px auto" src="/assets/images/arcadedb-vs-postgresql-lsqb.svg" alt="Bar chart of the nine LSQB queries: ArcadeDB is faster on every query, from 1.8x on Q2 to 161x on Q6 and several hundred times on Q4 and Q7" width="1000" height="660" loading="lazy" /></p>

<details>
  <summary>Show the numbers</summary>

  <table>
    <thead>
      <tr>
        <th>Query</th>
        <th>Pattern</th>
        <th style="text-align: right">PostgreSQL</th>
        <th style="text-align: right">ArcadeDB</th>
        <th style="text-align: right">ArcadeDB faster</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Q1</td>
        <td>8-hop chain: Country to TagClass</td>
        <td style="text-align: right">6.56 s</td>
        <td style="text-align: right">0.25 s</td>
        <td style="text-align: right">26x</td>
      </tr>
      <tr>
        <td>Q2</td>
        <td>Cycle: friends who reply to each other</td>
        <td style="text-align: right">0.34 s</td>
        <td style="text-align: right">0.19 s</td>
        <td style="text-align: right">1.8x</td>
      </tr>
      <tr>
        <td>Q3</td>
        <td>Triangle of friends in the same country</td>
        <td style="text-align: right">2.12 s</td>
        <td style="text-align: right">0.13 s</td>
        <td style="text-align: right">16x</td>
      </tr>
      <tr>
        <td>Q4</td>
        <td>Star: tagged message, creator, liker, reply</td>
        <td style="text-align: right">6.86 s</td>
        <td style="text-align: right">0.03 s</td>
        <td style="text-align: right">229x</td>
      </tr>
      <tr>
        <td>Q5</td>
        <td>Message and reply with different tags</td>
        <td style="text-align: right">0.69 s</td>
        <td style="text-align: right">0.23 s</td>
        <td style="text-align: right">3.0x</td>
      </tr>
      <tr>
        <td>Q6</td>
        <td>Friends of friends and their interests</td>
        <td style="text-align: right">17.72 s</td>
        <td style="text-align: right">0.11 s</td>
        <td style="text-align: right">161x</td>
      </tr>
      <tr>
        <td>Q7</td>
        <td>Q4 with optional likes and replies</td>
        <td style="text-align: right">11.22 s</td>
        <td style="text-align: right">0.02 s</td>
        <td style="text-align: right">561x</td>
      </tr>
      <tr>
        <td>Q8</td>
        <td>Q5 with a negative pattern</td>
        <td style="text-align: right">1.31 s</td>
        <td style="text-align: right">0.19 s</td>
        <td style="text-align: right">6.9x</td>
      </tr>
      <tr>
        <td>Q9</td>
        <td>Q6, excluding people who are already friends</td>
        <td style="text-align: right">22.25 s</td>
        <td style="text-align: right">1.06 s</td>
        <td style="text-align: right">21x</td>
      </tr>
      <tr>
        <td><strong>Total</strong></td>
        <td><strong>All nine queries</strong></td>
        <td style="text-align: right"><strong>69.07 s</strong></td>
        <td style="text-align: right"><strong>2.21 s</strong></td>
        <td style="text-align: right"><strong>31x</strong></td>
      </tr>
    </tbody>
  </table>

  <p><small>Both systems in Docker on the same host; ArcadeDB embedded totals 2.40 s. Times are rounded to 10 ms, so read the Q4 and Q7 ratios as “hundreds of times faster”, not as exact multipliers. Results for Neo4j, DuckDB, Kuzu, Memgraph, and Dgraph are on the <a href="/benchmarks.html">benchmarks page</a>.</small></p>

</details>

<p>ArcadeDB wins all nine, and the gap follows the shape of the query:</p>

<ul>
  <li><strong>Short patterns (Q2, Q5): 1.8x to 3x.</strong> Hash joins work here, and PostgreSQL does fine.</li>
  <li><strong>Friends of friends (Q6): 161x.</strong> Only two hops, but 1.67 billion matches: 17.7 seconds of self-joins against 110 milliseconds of walking adjacency lists.</li>
  <li><strong>Stars (Q4, Q7): hundreds of times.</strong> Every arm of the star is another join in PostgreSQL, and just another neighbor list of the same vertex in ArcadeDB.</li>
</ul>

<p>The <a href="https://memgraph.com/blog/postgresql-19-alternatives-memgraph">Memgraph team found the same curve</a> on a different dataset: comparable up to three hops, a PostgreSQL timeout at five.</p>

<h2 id="beyond-fixed-patterns-what-postgresql-cant-express-yet">Beyond Fixed Patterns: What PostgreSQL Can’t Express Yet</h2>

<p>LSQB uses fixed-length patterns only, the best case for SQL/PGQ. Real graph applications go further:</p>

<ul>
  <li><strong>Variable-length paths.</strong> “Within five hops of a known fraudster” needs a recursive CTE in PostgreSQL: 2.2 seconds on a 1 million node graph, and 6 seconds with emulated SQL/PGQ recursion, <a href="https://exobench.ai/blog/pg19-graph-queries-part-1">per exobench</a>. In ArcadeDB it is one line: <code class="language-plaintext highlighter-rouge">MATCH (a)-[:TRANSFER*1..5]-&gt;(b)</code>.</li>
  <li><strong>Graph algorithms.</strong> None in PostgreSQL. ArcadeDB is fastest on 5 of the 6 <a href="/benchmarks.html">LDBC Graphalytics</a> algorithms, with PageRank in 0.10 seconds.</li>
  <li><strong>Writes through the graph.</strong> <code class="language-plaintext highlighter-rouge">CREATE</code> and <code class="language-plaintext highlighter-rouge">MERGE</code> are ordinary transactional writes in ArcadeDB; in PostgreSQL the graph is a read-only view.</li>
  <li><strong>New relationship types.</strong> A new edge type in ArcadeDB; a new table, a migration, and an <code class="language-plaintext highlighter-rouge">ALTER PROPERTY GRAPH</code> in PostgreSQL.</li>
</ul>

<h2 id="but-i-dont-want-another-database">“But I Don’t Want Another Database”</h2>

<p>That is the real appeal of SQL/PGQ, and it is fair. But if the goal is one database, it doesn’t have to be PostgreSQL. ArcadeDB is a complete ACID DBMS, not a graph add-on: relational records with schemas and constraints, plus graph, documents, vectors, full-text, time series, and geospatial data in the same transaction. That is also what makes it a natural fit for <a href="/graph-rag.html">GraphRAG</a>.</p>

<p><img style="display:block;margin:32px auto" src="/assets/images/arcadedb-vs-postgresql-one-engine.svg" alt="ArcadeDB: clients connect via SQL, Cypher, Gremlin, PostgreSQL wire protocol, Neo4j Bolt, or HTTP; one engine stores relational records, graph, documents, key-value, full-text, vectors, time series, and geospatial data" width="1000" height="420" loading="lazy" /></p>

<p>Your team keeps its tools: <code class="language-plaintext highlighter-rouge">psql</code> and PostgreSQL drivers connect over the wire protocol. One honest caveat: queries are ArcadeDB SQL or Cypher, not PostgreSQL’s SQL dialect. So:</p>

<ol>
  <li><strong>PostgreSQL alone</strong> when relationships are a minor feature.</li>
  <li><strong>ArcadeDB alone</strong> for new applications or modest schemas: one engine, nothing to sync.</li>
  <li><strong>PostgreSQL plus ArcadeDB</strong> when an app is deeply tied to PostgreSQL SQL, PL/pgSQL, or extensions: keep it, and move the graph part, such as <a href="/fraud-detection.html">fraud rings</a>, <a href="/knowledge-graphs.html">knowledge graphs</a>, <a href="/recommendation-engine.html">recommendations</a>, <a href="/iam.html">access paths</a>, or <a href="/supply-chain.html">supply chains</a>, into ArcadeDB.</li>
</ol>

<h2 id="postgresql-vs-arcadedb-an-honest-guide">PostgreSQL vs ArcadeDB: An Honest Guide</h2>

<table>
  <thead>
    <tr>
      <th>Your workload</th>
      <th>PostgreSQL</th>
      <th>ArcadeDB</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Transactional records (users, orders, accounts)</td>
      <td>Good fit</td>
      <td>Good fit: ACID, schema, constraints, indexes</td>
    </tr>
    <tr>
      <td>Existing app built on PostgreSQL SQL, PL/pgSQL, or extensions</td>
      <td>Best fit</td>
      <td>Queries must be ported to ArcadeDB SQL or Cypher</td>
    </tr>
    <tr>
      <td>One or two hop lookups</td>
      <td>Good fit</td>
      <td>Good fit</td>
    </tr>
    <tr>
      <td>Long chains, cycles, or patterns that fan out</td>
      <td>16x to 560x slower than ArcadeDB in LSQB</td>
      <td>Built for it</td>
    </tr>
    <tr>
      <td>Variable-length paths (“within N hops”)</td>
      <td>Recursive CTEs only</td>
      <td>Native in Cypher, SQL, Gremlin</td>
    </tr>
    <tr>
      <td>Graph algorithms (PageRank, communities, shortest paths)</td>
      <td>Not available</td>
      <td>Native, LDBC Graphalytics leader</td>
    </tr>
    <tr>
      <td>Vectors, full-text search, time series, geospatial</td>
      <td>One extension per model</td>
      <td>Built in</td>
    </tr>
    <tr>
      <td>Graph plus vectors plus documents in one query</td>
      <td>Extensions and joins</td>
      <td>One engine, one transaction</td>
    </tr>
    <tr>
      <td>Relationship types that change every sprint</td>
      <td>Migration per change</td>
      <td>Schema-optional</td>
    </tr>
  </tbody>
</table>

<p>If relationships are a <em>feature</em> of your application, SQL/PGQ will be a welcome upgrade whenever it ships. If relationships <em>are</em> your application, a relational engine with graph syntax is still a relational engine.</p>

<h2 id="try-it-yourself">Try It Yourself</h2>

<p>The queries, datasets, and harness are public: see the <a href="/benchmarks.html">benchmarks page</a> and the <a href="https://github.com/ArcadeData/ldbc_graphalytics_platforms_arcadedb">runner on GitHub</a>. If your numbers differ, send them to us. To try your own workload:</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> 5432:5432 <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 -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin"</span> <span class="se">\</span>
  arcadedata/arcadedb:latest
</code></pre></div></div>

<p>Then connect with <code class="language-plaintext highlighter-rouge">psql</code>, or open the Studio at <code class="language-plaintext highlighter-rouge">http://localhost:2480</code> and write your first <code class="language-plaintext highlighter-rouge">MATCH</code>.</p>]]></content><author><name>Luca Garulli</name></author><category term="Graph Database" /><category term="PostgreSQL" /><category term="SQL/PGQ" /><category term="Benchmarks" /><category term="LDBC" /><category term="LSQB" /><category term="Comparison" /><category term="Postgres Graph" /><category term="Neo4j Alternative" /><summary type="html"><![CDATA[PostgreSQL 19 was going to ship SQL/PGQ graph queries, then reverted them. Even when they land, they compile to joins. We ran the LDBC LSQB benchmark: ArcadeDB finished all nine queries 31x faster than PostgreSQL.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/arcadedb-vs-postgresql-graph.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/arcadedb-vs-postgresql-graph.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB Is the Graph Database Behind Tom Sawyer Software’s Digital Engineering Suite</title><link href="https://arcadedb.com/blog/arcadedb-tom-sawyer-digital-engineering/" rel="alternate" type="text/html" title="ArcadeDB Is the Graph Database Behind Tom Sawyer Software’s Digital Engineering Suite" /><published>2026-09-23T00:00:00+00:00</published><updated>2026-09-23T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-tom-sawyer-digital-engineering</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-tom-sawyer-digital-engineering/"><![CDATA[<p>ArcadeDB is now the graph database backbone behind <a href="https://www.tomsawyer.com/digital-engineering">Tom Sawyer Digital Engineering</a>, Tom Sawyer Software’s modular suite for connecting engineering data across the product lifecycle. Every module in the suite, from Planning to Explorations, runs on ArcadeDB underneath. In Tom Sawyer Software’s own words: “ArcadeDB provides the open-source graph database backbone for Tom Sawyer Digital Engineering.”</p>

<p><img src="/assets/images/tomsawyer-crime-network-dashboard.jpg" alt="A Tom Sawyer Software graph analytics dashboard: a people-and-incidents network diagram, a full network graph, and linked charts breaking down incidents by type and precinct" />
<em>A Tom Sawyer Software graph analytics dashboard built on demo crime-network data: entity relationships, a full network graph, and linked incident charts, all views over the same connected data. Image: <a href="https://www.tomsawyer.com">tomsawyer.com</a>.</em></p>

<p>Tom Sawyer Software has built <a href="/knowledge-graphs.html">graph analytics</a> and visualization tools for over three decades, and picking a database to sit underneath a commercial engineering product is not a decision made lightly. The <a href="https://github.com/ArcadeData/arcadedb/blob/main/LICENSE">Apache 2.0 license</a> is a big part of why: customers get an open-source database with no proprietary licensing cost, and can still add commercial support with SLAs for production environments if they need it.</p>

<h2 id="what-digital-engineering-actually-does">What Digital Engineering actually does</h2>

<p>The suite targets a familiar problem in large engineering organizations: requirements, system models, processes, and change history end up scattered across disconnected tools, and nobody can trace a decision from one end of the product lifecycle to the other. Tom Sawyer Digital Engineering is built to break down those silos with six modules:</p>

<ul>
  <li><strong>Planning</strong>, for visual dependency and change-impact analysis.</li>
  <li><strong>Business Process</strong>, a BPMN 2.0-compliant workflow designer and execution engine.</li>
  <li><strong>Model-Based Engineering &amp; SysML v2 Viewer</strong>, which generates diagrams automatically from system models.</li>
  <li><strong>Data Streams</strong>, real-time ETL for normalizing data coming from disparate sources.</li>
  <li><strong>Perspectives</strong>, a low-code platform for building custom <a href="/knowledge-graphs.html">knowledge graph</a> applications on top of the same data.</li>
  <li><strong>Explorations</strong>, a no-code interface for querying the graph database directly, aimed at business stakeholders rather than engineers.</li>
</ul>

<p>Every one of those modules is a different way of looking at the same underlying graph: a requirement connects to the system model element that implements it, which connects to the process step that changes it, which connects to the person who approved the change. That is exactly the kind of interconnected, schema-flexible data a graph database is built to hold, and it is why a suite spanning six different front ends can share one backbone instead of six separate stores.</p>

<h2 id="why-this-fits-arcadedbs-model">Why this fits ArcadeDB’s model</h2>

<p>Tom Sawyer Software’s page frames ArcadeDB specifically as a graph database, but <a href="/knowledge-graphs.html">ArcadeDB is multi-model underneath</a>: the same engine that stores the graph also handles document, key-value, search, vector, and time-series data without a separate database to run or a separate query language to learn for each. For a suite where traceability, search, and analytics all touch the same records, that means Digital Engineering does not need to bolt on a second system when a module needs something other than graph traversal, whether that is full-text search over requirements or vector similarity for finding related model elements.</p>

<p>Digital Engineering is a web-based suite, which means it talks to its data store the way most web applications do: as a <a href="/client-server.html">client-server</a> deployment rather than an embedded library. That is the deployment mode ArcadeDB was built to run in, alongside the embedded mode used when a database needs to live inside another application’s process.</p>

<h2 id="an-open-source-backbone-with-a-commercial-path">An open-source backbone, with a commercial path</h2>

<p>The detail worth underlining is the licensing structure Tom Sawyer Software chose to build on. ArcadeDB’s Apache 2.0 license means Digital Engineering customers are not paying a database vendor on top of paying Tom Sawyer Software, and are not locked into a proprietary format if they ever want to inspect or migrate their own data. For teams that need guarantees beyond what a community can offer, commercial ArcadeDB support with service-level agreements is available as a separate, optional layer, so the choice of open source under the hood does not mean going without a support contract for production.</p>

<p>If you are evaluating a <a href="/knowledge-graphs.html">graph database for graph analytics</a> of your own, rather than through Tom Sawyer Software’s suite, the same traversal, pattern-matching, and multi-model engine described above is available directly: SQL, Cypher, and Gremlin against the same records, embedded or <a href="/client-server.html">client-server</a>, under the same Apache 2.0 license.</p>]]></content><author><name>Luca Garulli</name></author><category term="Partnership" /><category term="Ecosystem" /><category term="Graph Database" /><category term="Graph Analytics" /><category term="Digital Engineering" /><category term="MBSE" /><category term="SysML" /><category term="Integration" /><category term="Open Source" /><summary type="html"><![CDATA[Tom Sawyer Software built its Digital Engineering suite, Planning, Business Process, Model-Based Engineering, Data Streams, Perspectives, and Explorations, on ArcadeDB as the open-source graph analytics and graph database backbone.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/arcadedb-tom-sawyer-digital-engineering.jpg" /><media:content medium="image" url="https://arcadedb.com/assets/images/arcadedb-tom-sawyer-digital-engineering.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB in the Apache Arrow Ecosystem, Through Its PostgreSQL Wire Protocol</title><link href="https://arcadedb.com/blog/arcadedb-apache-arrow-adbc-postgresql-wire/" rel="alternate" type="text/html" title="ArcadeDB in the Apache Arrow Ecosystem, Through Its PostgreSQL Wire Protocol" /><published>2026-09-21T00:00:00+00:00</published><updated>2026-09-21T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-apache-arrow-adbc-postgresql-wire</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-apache-arrow-adbc-postgresql-wire/"><![CDATA[<p><em>By Prateek Singh, maintainer of <a href="https://adbcbridge.org/">adbcBridge</a>. An ecosystem and integration note: every issue, timestamp and number below is public. Facts as of 2026-09-21.</em></p>

<h2 id="the-short-version">The short version</h2>

<p>In August a new client stack started exercising ArcadeDB’s PostgreSQL wire protocol: Apache Arrow’s ADBC API, through adbcBridge and the standard PostgreSQL ODBC driver, and separately through Arrow’s own native PostgreSQL driver.</p>

<p>Over three weeks that testing found four gaps in the protocol implementation. ArcadeDB fixed the first in four and a half hours, the second in nine days, merged an external fix for the third the day it arrived, and opened the fourth on its own tracker with a root cause on the same day it was raised.</p>

<p>The wire protocol held up. Where it did not, the response was the story.</p>

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

<p><strong>1. The Arrow native PostgreSQL ADBC driver could not connect (<a href="https://github.com/ArcadeData/arcadedb/issues/7178">#7178</a>).</strong> That driver bootstraps its type table with a query against <code class="language-plaintext highlighter-rouge">pg_catalog.pg_type</code>, and ArcadeDB answered it with zero columns. Raised as a follow-up in <a href="https://github.com/ArcadeData/arcadedb/discussions/6888">discussion #6888</a>; the maintainers opened the issue themselves at 22:11 UTC on 2026-09-05 and closed it, fixed, at 02:48 UTC on 2026-09-06. Four hours and thirty-seven minutes.</p>

<p><strong>2. <code class="language-plaintext highlighter-rouge">COPY (query) TO STDOUT</code> was not supported (<a href="https://github.com/ArcadeData/arcadedb/issues/7188">#7188</a>).</strong> The native Arrow driver reads every result through binary <code class="language-plaintext highlighter-rouge">COPY</code> by default, so after the first fix it connected and then stopped at the first result set. Opened 2026-09-06, implemented in text and binary form and closed 2026-09-15. With that, Arrow’s own PostgreSQL driver reads ArcadeDB main with default options.</p>

<p><strong>3. Transaction-control statements answered with a row description (<a href="https://github.com/ArcadeData/arcadedb/pull/7815">#7815</a>).</strong> On the simple-query path, <code class="language-plaintext highlighter-rouge">BEGIN</code>, <code class="language-plaintext highlighter-rouge">COMMIT</code>, <code class="language-plaintext highlighter-rouge">ROLLBACK</code> and the savepoint statements were sent with an empty <code class="language-plaintext highlighter-rouge">RowDescription</code> before <code class="language-plaintext highlighter-rouge">CommandComplete</code>, so libpq reported a result set where it expected a command status. <code class="language-plaintext highlighter-rouge">psql</code> printed an empty table after <code class="language-plaintext highlighter-rouge">BEGIN</code>, and clients that start with autocommit off could not open a transaction at all. Sent as a pull request from the adbcBridge side on 2026-09-17, reviewed and merged the same day by a maintainer, who added a follow-up commit of his own.</p>

<p><strong>4. A datetime literal with a space separator and a fraction is stored as NULL (<a href="https://github.com/ArcadeData/arcadedb/issues/8090">#8090</a>).</strong> <code class="language-plaintext highlighter-rouge">'2024-02-29 13:45:10.123456'</code> is the form psqlodbc renders a bound timestamp in. The space alone parses, the fraction alone parses, both together fail to parse, and a blanket exception handler in the type converter turns the value into NULL while the <code class="language-plaintext highlighter-rouge">INSERT</code> reports success. Recorded on the adbcBridge side as an observed behaviour; the ArcadeDB team reproduced it on main, traced the root cause and opened the issue with a reproducer and a two-part fix proposal on 2026-09-21, the day it came up.</p>

<h2 id="why-a-client-stack-was-pushing-on-the-wire-protocol">Why a client stack was pushing on the wire protocol</h2>

<p>ADBC is Apache Arrow’s database connectivity API: one interface, columnar results, one driver per database. Most databases have no native ADBC driver, and ArcadeDB is one of them.</p>

<p>adbcBridge (<a href="https://adbcbridge.org/">adbcbridge.org</a>, Apache-2.0) implements ADBC on top of any ODBC driver, so a database with an ODBC driver becomes reachable from pandas, Polars, DuckDB, Rust, .NET, Java and Go through Arrow. ArcadeDB has no ODBC driver either, and does not need one: its PostgreSQL protocol plugin lets the standard PostgreSQL ODBC driver, psqlodbc, connect. adbcBridge drives psqlodbc; psqlodbc drives ArcadeDB.</p>

<p>That is the route that has passed adbcBridge’s compatibility checks on Linux, macOS and Windows since August. The second route, Arrow’s own PostgreSQL ADBC driver straight to ArcadeDB, is what findings 1 and 2 unblocked. Both of them lean on the same thing: a <a href="/client-server.html">client-server ArcadeDB</a> speaking a protocol that other people’s tools already know how to speak.</p>

<p><img src="/assets/images/arcadedb-arrow-adbc-routes.svg" alt="Two Arrow routes into ArcadeDB, both over its PostgreSQL wire protocol" /></p>

<h2 id="what-it-looks-like-from-python">What it looks like from Python</h2>

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

<span class="n">conn</span> <span class="o">=</span> <span class="n">adbcbridge</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span>
    <span class="n">uri</span><span class="o">=</span><span class="sh">"</span><span class="s">Driver={psqlodbcw.so};Server=127.0.0.1;Port=5432;</span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">Database=mydb;Uid=root;Pwd=...;BoolsAsChar=0;</span><span class="sh">"</span><span class="p">)</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">pandas</span><span class="p">.</span><span class="nf">read_sql</span><span class="p">(</span><span class="sh">"</span><span class="s">SELECT * FROM Product WHERE price &gt; 10</span><span class="sh">"</span><span class="p">,</span> <span class="n">conn</span><span class="p">)</span>
</code></pre></div></div>

<p>The same connection string works from Rust, C#, Java and Go through each language’s ADBC binding.</p>

<p><code class="language-plaintext highlighter-rouge">Driver=</code> names the psqlodbc library as installed on Linux and macOS; on Windows the driver is registered as <code class="language-plaintext highlighter-rouge">PostgreSQL Unicode(x64)</code>. <code class="language-plaintext highlighter-rouge">BoolsAsChar=0</code> is a psqlodbc setting, not an ArcadeDB one: without it the ODBC driver reports every <code class="language-plaintext highlighter-rouge">BOOLEAN</code> as a five-character string.</p>

<h2 id="where-it-stands">Where it stands</h2>

<p><strong>Compatibility.</strong> ArcadeDB through psqlodbc is a passing read-only entry in adbcBridge’s matrix on Linux, macOS (Apple Silicon) and Windows. The timestamp behaviour in #8090 and the boolean fix from <a href="https://github.com/ArcadeData/arcadedb/issues/6674">#6674</a> were re-verified on 26.9.1 on 2026-09-21 on Linux and macOS. The entry covers queries, parameters and catalog calls.</p>

<p><img src="/assets/images/arcadedb-adbcbridge-matrix-row.png" alt="The ArcadeDB row in the adbcBridge compatibility matrix: psqlodbc 16 over the PostgreSQL wire, passing read checks on Linux, macOS arm64 and Windows x64" /></p>

<p>It is read-only by design: ArcadeDB has no <code class="language-plaintext highlighter-rouge">CREATE TABLE</code>, its DDL is <code class="language-plaintext highlighter-rouge">CREATE DOCUMENT TYPE</code> plus one <code class="language-plaintext highlighter-rouge">CREATE PROPERTY</code> per column, so the table-creating bulk-ingest path does not apply.</p>

<p><strong>Two Arrow routes.</strong> adbcBridge over psqlodbc works against any released ArcadeDB. Arrow’s native PostgreSQL driver works against main after #7178 and #7188, with default options.</p>

<p><strong>Every fix went upstream.</strong> None of the four findings is worked around on the client side. Each was fixed or filed in ArcadeDB, so every PostgreSQL client benefits, not only Arrow users.</p>

<h2 id="throughput-with-the-method">Throughput, with the method</h2>

<p>ArcadeDB 26.9 (PostgreSQL wire) through psqlodbc 16, in a 1 GB Docker container on the same laptop (Intel Core i9-13900HK, 14 cores / 20 threads, 31 GiB, Linux Mint 22.3, unixODBC 2.3.12). One table (<code class="language-plaintext highlighter-rouge">id int32</code>, <code class="language-plaintext highlighter-rouge">val float64</code>, <code class="language-plaintext highlighter-rouge">txt utf8</code>, <code class="language-plaintext highlighter-rouge">dt date32</code>); <code class="language-plaintext highlighter-rouge">SELECT id, val, txt, dt</code> of 100,000 rows drained into each language’s own Arrow record batches through adbcBridge; native delegation off, so every row travels over ODBC; median of 3 timings after a warmup, one database per run.</p>

<p>There is no ingest figure: ArcadeDB has no <code class="language-plaintext highlighter-rouge">CREATE TABLE</code> over the wire.</p>

<table>
  <thead>
    <tr>
      <th>Language (ADBC binding)</th>
      <th>Fetch, rows/s</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Python</td>
      <td>397,502</td>
    </tr>
    <tr>
      <td>Rust</td>
      <td>327,191</td>
    </tr>
    <tr>
      <td>C#</td>
      <td>355,848</td>
    </tr>
    <tr>
      <td>Java</td>
      <td>332,503</td>
    </tr>
    <tr>
      <td>Go</td>
      <td>417,419</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/arcadedb-adbcbridge-language-benchmarks.png" alt="The ArcadeDB row in the adbcBridge five-language benchmark table, showing fetch throughput for Python, Rust, C#, Java and Go" /></p>

<p>The five land between 327,000 and 417,000 rows/s, close enough to say what matters: the driver behaves the same whichever language loads it, and the differences are the bindings’ own.</p>

<p>Servers run locally, so the numbers reflect the ODBC driver plus the database rather than a network, and they move with whatever else is running on the host; read them as ratios, not absolutes. If you want numbers measured the way a database vendor measures them, ArcadeDB publishes its own on the <a href="/benchmarks.html">benchmarks page</a>; these are a client-stack measurement and answer a different question.</p>

<p>Harness: <a href="https://github.com/singhpratech/adbcbridge/blob/main/bench/langbench.sh">bench/langbench.sh</a>. Published rows: <a href="https://github.com/singhpratech/adbcbridge/blob/main/bench/LANGUAGE_BENCHMARKS.md">bench/LANGUAGE_BENCHMARKS.md</a>. The ArcadeDB fixture is the <code class="language-plaintext highlighter-rouge">arcadedb</code> service in <a href="https://github.com/singhpratech/adbcbridge/blob/main/tests/compat/docker-compose.yml">tests/compat/docker-compose.yml</a>.</p>

<h2 id="links">Links</h2>

<p>The two ArcadeDB notes on adbcbridge.org: <a href="https://adbcbridge.org/notes/arcadedb-fixed-the-pg-type-bootstrap-within-a-day/">ArcadeDB fixed the pg_type bootstrap within a day</a> and <a href="https://adbcbridge.org/notes/arcadedb-timestamp-null-and-a-boolean-retest/">A timestamp that stores NULL and says it worked, and a boolean note that went stale</a>, which carries the 26.9.1 retest and the bound-parameter case.</p>

<p>The compatibility matrix: <a href="https://adbcbridge.org/">adbcbridge.org</a>. The repository: <a href="https://github.com/singhpratech/adbcbridge">github.com/singhpratech/adbcbridge</a>; questions and defects go to its issues.</p>

<hr />

<p><em>Published with the author’s permission. The findings above were reported through ArcadeDB’s public issue tracker, and <a href="https://github.com/ArcadeData/arcadedb/pull/7815">#7815</a> was contributed as a pull request by the adbcBridge project.</em></p>]]></content><author><name>Prateek Singh</name></author><category term="Apache Arrow" /><category term="ADBC" /><category term="PostgreSQL" /><category term="Integration" /><category term="Drivers" /><category term="Open Source" /><category term="Ecosystem" /><category term="Benchmarks" /><summary type="html"><![CDATA[A new Arrow client stack spent three weeks exercising ArcadeDB's PostgreSQL wire protocol and found four gaps. The first was fixed in four and a half hours, an external pull request was merged the day it arrived, and every fix went upstream.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/arcadedb-apache-arrow-adbc.svg" /><media:content medium="image" url="https://arcadedb.com/assets/images/arcadedb-apache-arrow-adbc.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ArcadeDB Joins Google’s MCP Toolbox for Databases</title><link href="https://arcadedb.com/blog/arcadedb-mcp-toolbox-for-databases/" rel="alternate" type="text/html" title="ArcadeDB Joins Google’s MCP Toolbox for Databases" /><published>2026-09-17T00:00:00+00:00</published><updated>2026-09-17T00:00:00+00:00</updated><id>https://arcadedb.com/blog/arcadedb-mcp-toolbox-for-databases</id><content type="html" xml:base="https://arcadedb.com/blog/arcadedb-mcp-toolbox-for-databases/"><![CDATA[<p>Google’s <a href="https://mcp-toolbox.dev/">MCP Toolbox for Databases</a> now ships a first-class ArcadeDB integration. Point any MCP-compatible agent, Claude, Cursor, or a custom LangChain/ADK/Genkit pipeline, at a running ArcadeDB instance, and it can query graph, document, vector, or time-series data in Cypher or SQL, with connection pooling, authentication, and OpenTelemetry observability handled by the toolbox rather than by your application code.</p>

<p>That last part is the detail worth sitting with. The <a href="https://github.com/googleapis/mcp-toolbox">Toolbox</a> (16.5k stars on GitHub, Apache 2.0, maintained by Google) is already the MCP server plenty of teams point at PostgreSQL, BigQuery, Snowflake, Spanner, and Neo4j. ArcadeDB is now in that same list. A team already running the Toolbox in front of Neo4j or Postgres doesn’t need to learn a new integration pattern to add ArcadeDB: it’s the same source-and-tool YAML shape, the same auth model, the same observability pipeline. That’s real distribution, not a blog-post announcement.</p>

<h2 id="what-the-toolbox-actually-does">What the Toolbox actually does</h2>

<p>MCP Toolbox for Databases solves a problem that shows up the moment you connect more than one agent to more than one database: every hand-rolled integration reinvents connection pooling, credential handling, and query safety. The Toolbox centralizes that. You describe a <strong>source</strong> (a database connection) and one or more <strong>tools</strong> (a named, scoped operation an agent can call) in YAML, and the Toolbox exposes those tools over MCP. Support for Google Sign-In and generic OIDC, Model Armor request screening, telemetry export, and read-only tool modes all come from the Toolbox itself, so an ArcadeDB integration inherits them for free.</p>

<h2 id="configuring-the-arcadedb-source">Configuring the ArcadeDB source</h2>

<p>A source points the Toolbox at a running ArcadeDB server over the Bolt protocol:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">kind</span><span class="pi">:</span> <span class="s">source</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">my-arcadedb-source</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">arcadedb</span>
<span class="na">uri</span><span class="pi">:</span> <span class="s">bolt://localhost:7687</span>
<span class="na">user</span><span class="pi">:</span> <span class="s">root</span>
<span class="na">password</span><span class="pi">:</span> <span class="s">${PASSWORD}</span>
<span class="na">database</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mydb"</span>
</code></pre></div></div>

<p>This assumes ArcadeDB running as a <a href="/client-server.html">server</a> with Bolt enabled, since the Toolbox connects to it the same way a Neo4j driver would. <code class="language-plaintext highlighter-rouge">${PASSWORD}</code> is environment-variable substitution: the Toolbox recommends it over hardcoding credentials in the YAML, which matters the moment this config file ends up in a repository.</p>

<h2 id="the-two-tools">The two tools</h2>

<p>With the source defined, the integration exposes exactly two tools.</p>

<p><code class="language-plaintext highlighter-rouge">arcadedb-execute-cypher</code> runs Cypher against the source:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">kind</span><span class="pi">:</span> <span class="s">tool</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">query_arcadedb</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">arcadedb-execute-cypher</span>
<span class="na">source</span><span class="pi">:</span> <span class="s">my-arcadedb-source</span>
<span class="na">readOnly</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
  <span class="s">Execute Cypher against ArcadeDB in read-only mode.</span>
  <span class="s">Example:</span>
  <span class="s">{{</span>
      <span class="s">"cypher": "MATCH (n) RETURN count(n)"</span>
  <span class="s">}}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">arcadedb-execute-sql</code> runs ArcadeDB SQL, which is where the multi-model part of ArcadeDB shows up: the same tool reaches <a href="/knowledge-graphs.html">graph and document data</a> in one query, since SQL and Cypher operate against the same underlying records.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">kind</span><span class="pi">:</span> <span class="s">tool</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">query_arcadedb_sql</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">arcadedb-execute-sql</span>
<span class="na">source</span><span class="pi">:</span> <span class="s">my-arcadedb-source</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">|</span>
  <span class="s">Execute SQL against ArcadeDB.</span>
  <span class="s">Example:</span>
  <span class="s">{{</span>
      <span class="s">"sql": "SELECT FROM Person WHERE name = :name LIMIT 5",</span>
      <span class="s">"params": {</span>
        <span class="s">"name": "Ada"</span>
      <span class="s">},</span>
      <span class="s">"dry_run": false</span>
  <span class="s">}}</span>
</code></pre></div></div>

<p>Both tools accept <code class="language-plaintext highlighter-rouge">readOnly</code> to block write statements and a per-call <code class="language-plaintext highlighter-rouge">dry_run</code> to validate a query without running it. Set <code class="language-plaintext highlighter-rouge">readOnly: true</code> unless the agent genuinely needs to write, the same way you’d scope any credential you hand to an LLM.</p>

<h2 id="not-a-replacement-for-the-built-in-mcp-server">Not a replacement for the built-in MCP server</h2>

<p>ArcadeDB has had its own <a href="/blog/arcadedb-mcp-server-connect-your-llm-to-your-database/">built-in MCP server since v26.3.1</a>, running inside the database engine with five tools covering schema discovery, querying across every supported query language, and writes with per-operation permission flags. That path is still the fastest way to connect a single AI client directly to a single ArcadeDB instance: nothing to deploy, native authentication, zero extra hops.</p>

<p>The Toolbox integration solves a different problem. It’s for teams that already standardized on the Toolbox as their agent-to-database layer, often because they’re already using it for other databases in the same stack. For them, ArcadeDB showing up as a source type means one less custom integration to maintain, and it means ArcadeDB gets evaluated in the same breath as PostgreSQL, Spanner, and Neo4j rather than needing a separate pitch for why an AI agent should be allowed to talk to it at all.</p>

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

<p>The full integration docs, including the generic prebuilt-config workflow the Toolbox uses for every source type, are at <a href="https://mcp-toolbox.dev/integrations/arcadedb/">mcp-toolbox.dev/integrations/arcadedb</a>. The Toolbox itself is on GitHub at <a href="https://github.com/googleapis/mcp-toolbox">googleapis/mcp-toolbox</a>.</p>]]></content><author><name>Luca Garulli</name></author><category term="MCP" /><category term="AI" /><category term="LLM" /><category term="Integration" /><category term="Google" /><category term="Cypher" /><category term="SQL" /><category term="Graph Database" /><category term="Agents" /><summary type="html"><![CDATA[ArcadeDB is now a first-class integration in Google's MCP Toolbox for Databases, the open-source MCP server that connects AI agents to enterprise databases alongside PostgreSQL, BigQuery, Spanner, Snowflake, and Neo4j.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://arcadedb.com/assets/images/arcadedb-mcp-toolbox.png" /><media:content medium="image" url="https://arcadedb.com/assets/images/arcadedb-mcp-toolbox.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>