<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Http on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/http/</link><description>Recent content in Http on MongrelDB</description><image><title>MongrelDB</title><url>https://www.mongreldb.com/assets/og-mongreldb.png</url><link>https://www.mongreldb.com/assets/og-mongreldb.png</link></image><generator>Hugo</generator><language>en-US</language><lastBuildDate>Tue, 18 Aug 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/http/index.xml" rel="self" type="application/rss+xml"/><item><title>A Typed Exception Hierarchy That Maps to HTTP</title><link>https://www.mongreldb.com/articles/2026/08/a-typed-exception-hierarchy-that-maps-to-http/</link><pubDate>Tue, 18 Aug 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/08/a-typed-exception-hierarchy-that-maps-to-http/</guid><description>The MongrelDB PHP client maps HTTP status codes to a small typed exception hierarchy, so a 401 is an AuthException, a 409 is a ConstraintException carrying the server&amp;#39;s error code and the failing operation index, and your catch blocks read like the failure modes they handle.</description><content:encoded><![CDATA[<p>Error handling is where a database client either respects your time or wastes it, because the moment something goes wrong in production you are not reading documentation, you are reading a stack trace, and the difference between &ldquo;the server said 409 because op 3 of your batch violated a unique key&rdquo; and &ldquo;DatabaseException: something failed&rdquo; is the difference between a two-minute fix and an hour of log spelunking. The MongrelDB PHP client speaks HTTP to the daemon, and HTTP already has a perfectly good vocabulary for failure, so rather than inventing a parallel taxonomy of database-flavored error codes, the client maps the status codes it receives onto a small set of typed exceptions, and your catch blocks end up reading like the failure modes they actually handle.</p>
<h2 id="one-base-class-five-shapes-of-failure">One base class, five shapes of failure</h2>
<p>Everything extends <code>MongrelDBException</code>, which extends <code>\Exception</code>, so a single catch at the boundary still works when you do not care about the specifics, and that is the entire hierarchy:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">MongrelDBException</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">ConnectionException</span>   <span style="color:#75715e">// daemon unreachable, network error
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">AuthException</span>         <span style="color:#75715e">// 401 Unauthorized, 403 Forbidden
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">NotFoundException</span>     <span style="color:#75715e">// 404 Not Found
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">├──</span> <span style="color:#a6e22e">ConstraintException</span>   <span style="color:#75715e">// 409 Conflict
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">└──</span> <span style="color:#a6e22e">QueryException</span>        <span style="color:#75715e">// 400 Bad Request, 500 Internal Server Error
</span></span></span></code></pre></div><p>The mapping lives in one place, a <code>match</code> on the response status inside the client, and it is deliberately boring; 401 and 403 both become <code>AuthException</code> even though the fixes differ, a 401 means your credentials were rejected while a 403 means you authenticated fine and simply lack the grant, because from the caller&rsquo;s side both are the same category of problem, an auth-layer failure you cannot retry past, and the message tells you which half of it you are looking at. Where the mapping gets opinionated is 404, which the client detects two ways: a 404 status maps to <code>NotFoundException</code> directly, and on top of that the client inspects the parsed error envelope for a <code>not found:</code> message prefix regardless of status, because &ldquo;that table does not exist&rdquo; deserves its own type even when the server phrases it inside a broader error envelope.</p>
<h2 id="the-one-that-carries-real-weight-constraintexception">The one that carries real weight: ConstraintException</h2>
<p>Most of the hierarchy is just names, but <code>ConstraintException</code> earns its existence by carrying two extra properties, <code>errorCode</code> and <code>opIndex</code>, and those two fields are what turn a batch failure from a mystery into a diff:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Visorcraft\MongrelDB\Exceptions\ConstraintException</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    $tx <span style="color:#f92672">=</span> $db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">beginTransaction</span>();
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">insert</span>(<span style="color:#e6db74">&#39;users&#39;</span>, [<span style="color:#e6db74">&#39;email&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;ada@example.com&#39;</span>, <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Ada&#39;</span>]);
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">insert</span>(<span style="color:#e6db74">&#39;users&#39;</span>, [<span style="color:#e6db74">&#39;email&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;grace@example.com&#39;</span>, <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Grace&#39;</span>]);
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">insert</span>(<span style="color:#e6db74">&#39;users&#39;</span>, [<span style="color:#e6db74">&#39;email&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;ada@example.com&#39;</span>, <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Ada again&#39;</span>]);
</span></span><span style="display:flex;"><span>    $tx<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">commit</span>();
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">ConstraintException</span> $e) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// $e-&gt;errorCode === &#39;UNIQUE_VIOLATION&#39;
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">// $e-&gt;opIndex  === 2
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">// the whole batch rolled back atomically
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>errorCode</code> is the server&rsquo;s own string code, <code>UNIQUE_VIOLATION</code>, <code>FK_VIOLATION</code>, <code>CHECK_VIOLATION</code>, <code>TRIGGER_VALIDATION</code>, or the generic <code>CONFLICT</code>, and the <code>opIndex</code> tells you which operation inside the atomic batch tripped the constraint, zero-based, so when a forty-operation transaction fails you know exactly which insert to look at instead of bisecting your own code. Because the engine evaluates constraints at commit time and the batch is atomic, a <code>ConstraintException</code> also carries a guarantee you can build on: nothing partially applied, no cleanup pass, the database is exactly where it was before <code>commit()</code> was called, which is the property that makes retrying safe once you have fixed the offending row.</p>
<h2 id="why-status-shaped-exceptions-beat-a-single-databaseexception">Why status-shaped exceptions beat a single DatabaseException</h2>
<p>The old PHP database extensions trained a generation of us to check return values, <code>mysql_query</code> returned <code>false</code> and you called <code>mysql_error()</code> and hoped the string was parseable, and the PDO era improved that to a single exception class with a SQLSTATE code stuffed in <code>getCode()</code>, which meant every serious codebase grew its own switch statement mapping <code>23000</code> to &ldquo;probably a duplicate key.&rdquo; The shape of the failure was always there, it was just encoded in a place the type system could not see, so the compiler could not help you and your IDE could not autocomplete the recovery path. Putting the shape in the class name instead means <code>catch (AuthException)</code> is a complete sentence, it means a static analyzer with unchecked-exception tracking enabled can tell you that you handle constraint violations but not connection failures, and it means the retry policy for a transient <code>ConnectionException</code> never accidentally swallows a permanent <code>QueryException</code>, because those are different types and PHP&rsquo;s catch semantics keep them apart for free.</p>
<p>There is a tradeoff worth naming, and it is that the hierarchy is shallow on purpose; the client does not try to give every server error code its own class, because a deep hierarchy is its own kind of failure, the kind where you version your exceptions and break everyone&rsquo;s catch blocks on a minor release. Five leaves and one rich class is the line we picked, the wire stays the source of truth through <code>errorCode</code> for anything finer-grained, and if the daemon ever grows a genuinely new failure mode that callers must handle differently, that is a deliberate API change and not an accident of a new status string. That is the whole design: HTTP already classified the error, the client just refuses to throw the classification away on the way into PHP.</p>
]]></content:encoded></item><item><title>Daemon HTTP Endpoints: The Wire Contract</title><link>https://www.mongreldb.com/articles/2026/08/daemon-http-endpoints-the-wire-contract/</link><pubDate>Sat, 15 Aug 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/08/daemon-http-endpoints-the-wire-contract/</guid><description>A walk through the actual HTTP surface mongreldb-server exposes, from /sql and /txn to sessions, pagination cursors, and the /kit/* typed routes, and why plain HTTP was the right transport bet.</description><content:encoded><![CDATA[<p>A database wire protocol is the kind of decision you make once and then live inside for a decade, because every client you ever ship, every proxy you ever deploy behind, and every debugging session at 2am is shaped by it, and the industry default for thirty years has been to invent a binary protocol per database and then spend the next decade writing drivers for it. Postgres has its frontend/backend protocol, MySQL has its own packet format, Redis got away with RESP because it was simple enough to type by hand, and each of those choices created a whole ecosystem of client libraries that had to be ported, maintained, and debugged in every language somebody wanted to use. When we built the server mode for MongrelDB the tempting move was the same one, a compact binary framing over TCP with a Rust reference client, and we rejected it for a reason that has only gotten stronger since: plain HTTP with JSON bodies is already implemented, already proxied, already authenticated, and already debuggable with <code>curl</code> in every environment your code will ever run in, and the overhead it costs you is almost never where your latency budget actually goes.</p>
<p>That choice has a consequence worth stating up front, which is that the HTTP surface is not an accident or a shim, it is the contract, the same one the PHP client, the Kit SDKs, and your own shell scripts all speak, so it is worth walking through what <code>mongreldb-server</code> actually exposes and why the surface is shaped the way it is.</p>
<h2 id="the-shape-of-the-surface">The shape of the surface</h2>
<p>The daemon is an axum router, and the routes fall into a few deliberate groups rather than one flat pile. The operational group is what your monitoring talks to: <code>/health</code> for liveness, <code>/build-info</code> for the exact version and build metadata, <code>/capabilities</code> for what this build supports, <code>/metrics</code> for Prometheus-style scraping, <code>/audit</code> for the audit log, and <code>/history/retention</code> for reading and setting retention policy. Admin operations sit under <code>/admin/drain</code> and <code>/admin/reload</code>, with cluster membership workflows under <code>/admin/cluster/*</code> when the binary is built with the cluster feature, and that grouping exists so your firewall rules and your reverse proxy ACLs can treat &ldquo;is it alive&rdquo; traffic and &ldquo;reconfigure the server&rdquo; traffic as different trust levels, because they are.</p>
<p>The data group is the part a client touches on every request, and it starts with the table primitives: <code>GET</code> and <code>POST /tables</code> to list and create, <code>DELETE /tables/{name}</code> to drop, <code>POST /tables/{name}/put</code> to write a row, <code>GET /tables/{name}/count</code>, and <code>POST /tables/{name}/commit</code>. On top of that sits the SQL surface, which is where most real work happens, a single <code>POST /sql</code> that takes a JSON body and returns a JSON result, plus <code>POST /sql/continue</code> for paged results, <code>GET /queries/{query_id}</code> for status on a running query, and <code>POST /queries/{query_id}/cancel</code> when you want it dead.</p>
<h2 id="the-sql-request-is-the-contract">The /sql request is the contract</h2>
<p>The request body for <code>/sql</code> is where the interesting engineering lives, because a single POST carrying a SQL string is what every toy HTTP database does, and the difference between a toy and a transport is everything else in the envelope:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;sql&#34;</span>: <span style="color:#e6db74">&#34;UPDATE products SET price = price * 0.9 WHERE category = &#39;clearance&#39;&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;format&#34;</span>: <span style="color:#e6db74">&#34;json&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;query_id&#34;</span>: <span style="color:#e6db74">&#34;q-7f3a2c&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;timeout_ms&#34;</span>: <span style="color:#ae81ff">5000</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;max_output_rows&#34;</span>: <span style="color:#ae81ff">10000</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;max_output_bytes&#34;</span>: <span style="color:#ae81ff">4194304</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;idempotency_key&#34;</span>: <span style="color:#e6db74">&#34;import-run-42-batch-7&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Every field except <code>sql</code> is optional, and each one exists because a real failure mode demanded it. <code>timeout_ms</code> and the output limits mean a runaway statement dies on the server&rsquo;s terms rather than streaming gigabytes into a client that stopped caring, the <code>query_id</code> ties the request to the <code>/queries/{query_id}/cancel</code> endpoint so an operator or a watchdog can kill it from a different connection entirely, and <code>idempotency_key</code> is the storage-layer dedupe we covered in an earlier post, so a retry after a network drop replays the receipt instead of re-executing the write, which is exactly the property a batch import like the one above needs. The <code>format</code> field is a different kind of knob, a per-request serialization choice rather than a guard: JSON is the default, but <code>&quot;arrow&quot;</code> returns Arrow IPC file bytes instead, which is the honest answer to &ldquo;JSON text is slow&rdquo; for the analytical case where you are moving real row volume, same endpoint, same auth, same query, different bytes on the wire.</p>
<p>Reads ride the same envelope, and a paged SELECT adds a pagination block to the request: <code>page_size_rows</code> bounds by count, <code>max_page_bytes</code> bounds by serialized size, and <code>max_page_tokens</code> bounds by an estimated token count the server computes as <code>ceil(projected_json_bytes / 4)</code>, which is a crude heuristic and deliberately documented as one in the response itself, but crude and server-side beats every client inventing its own truncation logic after the damage is done.</p>
<h2 id="cursors-that-tell-the-truth">Cursors that tell the truth</h2>
<p>A paged response comes back with the page and a continuation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;completed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;rows&#34;</span>: [ <span style="color:#960050;background-color:#1e0010">/*</span> <span style="color:#960050;background-color:#1e0010">up</span> <span style="color:#960050;background-color:#1e0010">to</span> <span style="color:#960050;background-color:#1e0010">page_size_rows</span> <span style="color:#960050;background-color:#1e0010">row</span> <span style="color:#960050;background-color:#1e0010">objects</span> <span style="color:#960050;background-color:#1e0010">*/</span> ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;next_cursor&#34;</span>: <span style="color:#e6db74">&#34;9f2c...&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;page&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;offset&#34;</span>: <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;row_count&#34;</span>: <span style="color:#ae81ff">500</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;total_rows&#34;</span>: <span style="color:#ae81ff">18342</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;byte_count&#34;</span>: <span style="color:#ae81ff">241118</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;estimated_tokens&#34;</span>: <span style="color:#ae81ff">60280</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;expires_at_ms&#34;</span>: <span style="color:#ae81ff">1786963200000</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;snapshot&#34;</span>: <span style="color:#e6db74">&#34;retained_result&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;token_estimate&#34;</span>: <span style="color:#e6db74">&#34;ceil(projected_json_bytes/4)&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Yes, the server really ships the formula itself as a literal string in every paged response, and that is deliberate rather than redundant: any client reading <code>estimated_tokens</code> can see exactly how the number was produced without opening our docs, so nobody mistakes the estimate for a precise tokenizer count.</p>
<p>You pass <code>next_cursor</code> to <code>POST /sql/continue</code> and get the next page of the same retained result set, which matters because the snapshot is held server-side, so page two sees the same data page one saw even if writes landed in between. The cursor is HMAC-signed with a process-local key, and that detail is a deliberate tradeoff rather than an oversight: a tampered cursor is rejected instead of being treated as a query, and a server restart invalidates outstanding cursors, which means a client must be prepared to re-run the query after a failover. We could have made cursors durable across restarts, but that would mean persisting retained result sets or making cursors replayable queries with all the consistency questions that brings, and the honest contract is the simpler one: a cursor is a short-lived handle to a snapshot this process is holding for you, with an <code>expires_at_ms</code> that tells you exactly how short-lived, and when it is gone you start over.</p>
<h2 id="transactions-sessions-and-the-typed-routes">Transactions, sessions, and the typed routes</h2>
<p>Single-statement SQL is not the whole story, so <code>POST /txn</code> takes a batch of operations and applies them atomically, which is how the Kit layer implements its transaction commit and how any client can get all-or-nothing semantics over a transport that has no concept of a connection-held transaction. For callers that want the classic prepare-then-execute shape, the <code>/sessions</code> group provides it over HTTP: <code>POST /sessions</code> opens a session, <code>POST /sessions/{id}/prepare</code> registers a statement, <code>POST /sessions/{id}/execute</code> runs it with bound arguments, and <code>DELETE</code> on the statement or the session cleans up, so prepared-statement caching and parameter binding work the way you expect without the transport needing to keep a socket open per client.</p>
<p>The <code>/kit/*</code> group is the typed surface the SDKs prefer, routes like <code>/kit/schema</code>, <code>/kit/txn</code>, <code>/kit/query</code>, <code>/kit/search</code>, <code>/kit/retrieve</code>, <code>/kit/ann_rerank</code>, and <code>/kit/set_similarity</code>, and the distinction from raw <code>/sql</code> is that these carry structured, validated payloads rather than SQL text, so the engine can enforce constraints and column types at the boundary and the client never has to serialize a query string. Stored procedures and triggers get their own REST-shaped resources, <code>/procedures</code> and <code>/triggers</code> with list, create, describe, replace, drop, and call, which keeps the admin-y operations discoverable with nothing more exotic than an HTTP client. And for the replication story, <code>GET /wal/stream</code>, <code>GET /replication/snapshot</code>, and <code>GET /events</code> are the streaming edges, the places where HTTP&rsquo;s request-response shape bends into a long-lived stream because change capture genuinely is a stream.</p>
<h2 id="what-http-costs-and-what-it-buys">What HTTP costs, and what it buys</h2>
<p>The costs are real and worth naming. Every request carries headers and JSON parsing that a binary framing would shrink, there is no server push outside the streaming endpoints, and keep-alive and connection reuse are your responsibility through whatever HTTP client you bring, which is why the PHP client&rsquo;s persistent cURL sharing on 8.5+ matters as much as it does. The server meets you halfway on the abuse cases: a request-bytes bound rejects oversized bodies with a structured 413 rather than an OOM, and an optional concurrency limit sheds load instead of queueing until the wheels come off.</p>
<p>What it buys is everything you did not have to build. Auth is middleware with three modes, a bearer token, HTTP Basic verified against the Argon2id-hashed catalog users, or both, and TLS, rate limiting, and request logging come from whatever proxy you already run rather than from database-specific features somebody has to reimplement. Debugging is <code>curl</code> from any machine that can reach the port, load balancing is the same layer-7 infrastructure the rest of your stack uses, and a new language gets a working client the day somebody writes a hundred lines around its standard HTTP library, which is exactly how the long tail of MongrelDB language clients exists at all. The twenty-year view is that every database that invented a wire protocol also invented a driver maintenance burden it is still paying down, and the modern equivalent of the <code>mysql_*</code> extension problem is a proprietary protocol that only three of your twenty supported languages talk fluently; HTTP is not the fastest transport you can imagine, it is the fastest one you can already use from everywhere, and for an engine whose whole pitch is meeting you where your code runs, that is the right trade.</p>
]]></content:encoded></item><item><title>Embedded and Server Modes from One Database Engine</title><link>https://www.mongreldb.com/articles/2026/07/embedded-and-server-from-the-same-code-paths/</link><pubDate>Wed, 15 Jul 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/07/embedded-and-server-from-the-same-code-paths/</guid><description>MongrelDB runs in-process or behind mongreldb-server, keeping one storage and query engine while changing ownership, authentication, serialization, and resource boundaries.</description><content:encoded><![CDATA[<p>An embedded database and a server database may share a storage format while living under different operational rules, because an in-process caller can hold a native handle and share memory directly, while a server has to authenticate identities, preserve session state across requests, bound concurrent work, serialize results, and own the storage root on behalf of many clients; MongrelDB keeps one engine under both modes, but it does not pretend the boundaries are interchangeable.</p>
<p>The engine crates own WAL, MVCC, tables, indexes, transactions, compaction, backup, recovery, and DataFusion query execution. Embedded clients call those capabilities through native Rust, NAPI, Python, C ABI, or JNI surfaces. <code>mongreldb-server</code> links the same engine and adds sessions, credentials, HTTP or gRPC transport, resource ceilings, and multi-process ownership.</p>
<h2 id="embedded-mode-owns-the-root-directly">Embedded mode owns the root directly</h2>
<p>A native application opens a database directory and the engine takes an exclusive lock on that storage core. Threads inside the process share the same <code>Database</code>; identity-specific handles can share one core through the database manager, but a second independent process cannot open the same root and hope filesystem locking will merge their transaction histories.</p>
<p>This is the right shape for desktop applications, local-first tools, edge jobs, test harnesses, and services where one process already owns the lifecycle. Calls avoid a transport hop, typed values stay native longer, and Arrow IPC can move columnar results without turning every cell into a JSON object.</p>
<p>The cost is application ownership. A process crash is also a database-process crash, native artifacts must ship for the target platform, blocking work must leave latency-sensitive runtime threads, and backup or maintenance has to respect the open handle.</p>
<h2 id="server-mode-centralizes-ownership">Server mode centralizes ownership</h2>
<p><code>mongreldb-server</code> opens the root once and accepts multiple client processes. The typed Kit HTTP surface covers schema, transactions, query, scored retrieval, search, and maintenance. Native gRPC carries Protobuf control messages and Arrow IPC batches over HTTP/2 with TLS. A MySQL-compatible listener supports existing client workflows for the SQL surface.</p>
<p>The server boundary is where authentication, row and column policy, request deadlines, AI candidate ceilings, session limits, cancellation, and audit behavior become centralized instead of repeated in every application process. Language-native HTTP clients can stay pure Clojure, Go, PHP, Ruby, Swift, or Zig while one Rust daemon carries the engine.</p>
<p>The cost is the service itself. Somebody must start it, stop it, upgrade it, monitor it, provision TLS and credentials, and decide what happens when the process is unavailable. Local HTTP is simpler than a remote cluster, but it is still a network protocol and an operational component.</p>
<h2 id="shared-semantics-do-not-require-identical-apis">Shared semantics do not require identical APIs</h2>
<p>Both modes use the same tables, RowIds, index families, transactions, and SQL planner, but the safest public surface can differ. Trusted embedded SQL may use a Boolean ANN predicate because the host application owns the work. Remote ranked queries use scored table functions and typed endpoints with deadlines and candidate ceilings so one request cannot turn an authenticated server into an unbounded vector worker.</p>
<p>Likewise, an embedded handle can preserve session-scoped views and prepared state for its lifetime, while a stateless HTTP route may create a fresh SQL session unless the server protocol explicitly associates requests with a durable session. The engine behavior is shared; connection lifetime is not.</p>
<p>Trying to hide those differences behind one magical interface would make the simple embedded path carry server concepts and the server path inherit unsafe local assumptions. The useful contract is same storage truth and query meaning, with an honest boundary around ownership and transport.</p>
<h2 id="the-latency-evidence">The latency evidence</h2>
<p>The current qualification fixtures measure a warm embedded begin/get/rollback point path at 1.037 microseconds p50 and 1.434 microseconds p95 on the documented benchmark host. A warm loopback HTTP SQL point query measured 1.287 milliseconds p50, 2.070 milliseconds p95, and 2.396 milliseconds p99 in one current fixture.</p>
<p>Those are not a protocol microbenchmark over identical work. The HTTP result includes request handling, session lookup, SQL planning, and JSON serialization, while the embedded result is a native point operation. The numbers show the scale of the boundary, not a clean subtraction that assigns every extra microsecond to TCP.</p>
<p>A service doing one row per request will feel that boundary more than a service moving one Arrow batch or committing a useful transaction per request. Batching is not a workaround for a slow server; it is how a remote database protocol stops making transport overhead the unit of work.</p>
<h2 id="choose-ownership-before-syntax">Choose ownership before syntax</h2>
<p>Embed when one process can own the root, native distribution is acceptable, and local-call latency matters. Run the server when many processes or languages need the same database, when centralized credentials and limits matter, or when native packaging is more expensive than one local daemon.</p>
<p>MongrelDB Viewer supports both boundaries. Direct mode embeds the official engine and takes the root lock; Server mode connects through the official client and leaves ownership with the daemon. The <a href="https://www.mongreldb.com/local-vector-database-gui/">Viewer guide</a> compares which maintenance operations are available through each path.</p>
<p>A project can start embedded and move to the daemon without converting the storage model, but application code still has to acknowledge that a function call became a request, that identities and failure modes changed, and that a remote result can time out after the server committed work. Same engine is a useful migration property. It is not permission to ignore the network.</p>
<p>Current deployment and client details live in the <a href="https://www.mongreldb.com/languages.html">35-language matrix</a> and the engine <a href="https://github.com/visorcraft/MongrelDB/blob/master/README.md"><code>README.md</code></a>.</p>
]]></content:encoded></item><item><title>35 Languages, Two Boundaries, One MongrelDB Engine</title><link>https://www.mongreldb.com/articles/2026/07/mongreldb-client-architecture/</link><pubDate>Wed, 08 Jul 2026 18:30:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/07/mongreldb-client-architecture/</guid><description>MongrelDB supports 35 languages through nine embedded native bindings and 26 language-native HTTP clients, with one engine contract under both boundaries.</description><content:encoded><![CDATA[<p>Supporting a language means more than placing a generated client in a repository, because somebody eventually has to install it on an ARM laptop, pass a 64-bit RowId without truncating it through a JavaScript number, decode an Arrow batch, recover a transaction error, and upgrade the engine without discovering that the binding quietly depended on an internal Rust layout; the public count is now 35 languages, but the architecture is still two boundaries, embedded when the runtime can carry the engine safely and HTTP when portability is worth the local round trip.</p>
<p>The full, current package and repository matrix lives on the <a href="https://www.mongreldb.com/languages.html">MongrelDB language clients page</a>. This article is about why the matrix has two tiers instead of pretending every language should load the same native library.</p>
<h2 id="tier-1-keeps-the-engine-in-process">Tier 1 keeps the engine in-process</h2>
<p>Nine clients can run MongrelDB inside the application process: Rust, TypeScript and Node.js, Python, C, C++, C# and .NET, Java, Kotlin, and Scala. They reach the engine through one of the native boundaries already supported by their runtime:</p>
<ul>
<li>Rust calls the engine crates directly.</li>
<li>Node.js uses NAPI and returns large identifiers as <code>BigInt</code>.</li>
<li>Python uses its native extension path.</li>
<li>C and C++ use the stable C ABI.</li>
<li>C# and .NET bind the C ABI through their native interop layer.</li>
<li>Java, Kotlin, and Scala use the JNI shim.</li>
</ul>
<p>The reward is no HTTP serialization or socket hop, direct access to native query calls, and efficient Arrow IPC movement for columnar results. The cost is packaging: every supported OS and architecture needs the right native artifact or a documented source-build path, and a client release has to stay on the same compatibility train as the engine it embeds.</p>
<p>The Node addon currently publishes prebuilt Linux x64 and arm64 binaries. Other platforms build from source or use <code>RemoteDatabase</code> against the daemon. That caveat belongs next to the install command, not in a troubleshooting page discovered after <code>npm install</code> fails on a release machine.</p>
<h2 id="tier-2-keeps-the-language-native">Tier 2 keeps the language native</h2>
<p>Twenty-six clients connect to a running <code>mongreldb-server</code> over HTTP: Clojure, Crystal, D, Dart, Elixir, Erlang, F#, Fortran, Gleam, Go, Julia, Kotlin/Native, Lua, Mojo, Nim, Objective-C, Odin, Perl, PHP, PowerShell, R, Ruby, Swift, Tcl, V, and Zig.</p>
<p>Those packages use the language&rsquo;s ordinary HTTP and JSON stack, so installing the client does not pull in a Rust compiler, linker, JNI library, platform-specific DLL, or NAPI binary. The daemon owns the database root, keeps the engine warm, enforces sessions and credentials, and supports multiple client processes without asking them to coordinate exclusive filesystem locks.</p>
<p>The cost is real. A local HTTP query crosses request routing, authentication, SQL planning or typed endpoint dispatch, JSON or Arrow serialization, and the loopback network stack. The Stage 1 qualification on the published benchmark host measured a warm loopback SQL point query at 1.287 milliseconds p50, 2.070 milliseconds p95, and 2.396 milliseconds p99; the warm embedded begin/get/rollback path on the same class of evidence measured 1.037 microseconds p50 and 1.434 microseconds p95, though those paths do not perform identical work and should not be presented as a clean protocol-only ratio.</p>
<p>The lesson is smaller than the numbers: use the native boundary when per-call latency and in-process ownership matter, and use the daemon when installation, multi-process access, or language reach matters more.</p>
<h2 id="one-c-abi-supports-several-runtimes">One C ABI supports several runtimes</h2>
<p>The <code>mongreldb-ffi</code> crate exposes opaque handles, typed values, queries, transactions, authentication, DataFusion SQL returning Arrow IPC, and migration planning through a C-compatible boundary. <code>mongreldb-kit-ffi</code> adds schema, migration-runner, and query-builder operations, while <code>mongreldb-jni</code> gives JVM languages a dedicated shim over the same engine behavior.</p>
<p>Opaque handles matter because Rust structs are not an ABI. A C header can remain stable while internal ownership, caches, schedulers, and index implementations change behind it; the binding holds a handle, calls a documented function, receives a documented result, and never depends on the byte layout of an <code>Arc&lt;Database&gt;</code>.</p>
<p>This is less glamorous than generating thirty-five clients from one schema, but it is the difference between a language list and a language contract.</p>
<h2 id="the-engine-contract-stays-the-same">The engine contract stays the same</h2>
<p>Both tiers expose the same storage concepts: database, table, schema, typed values, transaction, condition, SQL, and maintenance. The exact convenience layer varies by language, and not every binding has a typed helper for every scored retrieval surface, but the underlying index families and durability model do not change because a caller moved from Rust to PHP.</p>
<p>Remote ranked AI queries use bounded scored endpoints so the server can enforce deadlines, maximum work, candidate ceilings, and concurrency. Embedded code can use direct native conditions where the trusted application owns those limits. This is one of the places where forcing byte-for-byte API symmetry would be worse than preserving the same result semantics with a safer remote boundary.</p>
<h2 id="choosing-a-boundary">Choosing a boundary</h2>
<p>Use embedded mode when one process can own the storage root, the application can ship the native artifact, and local-call latency matters. Use the daemon when several processes or languages need the same database, when the runtime&rsquo;s native packaging story is worse than operating one local service, or when centralized authentication and resource limits are required.</p>
<p>A desktop application may embed the engine and inspect it with <a href="https://www.mongreldb.com/local-vector-database-gui/">MongrelDB Viewer</a>. A service fleet may run one authenticated daemon and use language-native HTTP clients. A Node product may embed during local development and use <code>RemoteDatabase</code> in a multi-process deployment. The storage model remains the same, but the ownership boundary changes, which is exactly what a client architecture should make explicit.</p>
<p>Current install names, package status, and repository links are maintained on the <a href="https://www.mongreldb.com/languages.html">35-language matrix</a>, because counts change and an old article should not be the package registry.</p>
]]></content:encoded></item></channel></rss>