<?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>Stored-Procedures on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/stored-procedures/</link><description>Recent content in Stored-Procedures 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>Fri, 04 Sep 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/stored-procedures/index.xml" rel="self" type="application/rss+xml"/><item><title>Stored Procedures from PHP: Install, List, Drop, Call</title><link>https://www.mongreldb.com/articles/2026/09/stored-procedures-install-list-drop-call/</link><pubDate>Fri, 04 Sep 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/09/stored-procedures-install-list-drop-call/</guid><description>MongrelDB stored procedures are versioned, checksummed definitions installed over HTTP, and the pure PHP client exposes the whole lifecycle as five typed calls whose name-keyed argument arrays map directly onto the procedure&amp;#39;s declared params.</description><content:encoded><![CDATA[<p>Stored procedures carry a bad reputation they mostly earned, because the classic version of the idea was a pile of T-SQL or PL/pgSQL living in the catalog, edited in production by whoever had the password, versioned by nobody, and reviewed never; but the reason the idea existed in the first place is still sound, which is that some operations want to happen next to the data in one round trip with one commit, and shipping three queries over the wire so your application code can glue them together is the part that was always wrong, not the procedure. MongrelDB&rsquo;s take on it drops the string-of-SQL-in-the-catalog model entirely: a procedure is a structured definition with a name, a version, a mode, typed params, and a body made of ordered steps, the server checksums it when you install it, and the PHP client drives the whole lifecycle with five methods, <code>createProcedure</code>, <code>procedures</code>, <code>procedure</code>, <code>callProcedure</code>, and <code>dropProcedure</code>, which is the entire surface and is worth walking through because the shape of the definition is where the design decisions live.</p>
<h2 id="what-a-procedure-is-in-mongreldb">What a procedure is in MongrelDB</h2>
<p>The definition you install is an array that mirrors the engine&rsquo;s <code>StoredProcedure</code> struct field for field, and the only fields you do not own are the ones the server recomputes on install, so you send placeholders for those and it fills in the truth:</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>$db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">createProcedure</span>([
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;users_by_status&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;version&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;mode&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;read_only&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;params&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>        [<span style="color:#e6db74">&#39;name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;status&#39;</span>, <span style="color:#e6db74">&#39;ty&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;varchar&#39;</span>, <span style="color:#e6db74">&#39;nullable&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>],
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;body&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;steps&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>            [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;kind&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;native_query&#39;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;id&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;scan&#39;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;table&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;users&#39;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;conditions&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>                    [
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#39;kind&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;bitmap_eq&#39;</span>,
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#39;column_id&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">3</span>,
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#39;value&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;kind&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;param&#39;</span>, <span style="color:#e6db74">&#39;value&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;status&#39;</span>],
</span></span><span style="display:flex;"><span>                    ],
</span></span><span style="display:flex;"><span>                ],
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#39;limit&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">100</span>,
</span></span><span style="display:flex;"><span>            ],
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;return_value&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;kind&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;step_rows&#39;</span>, <span style="color:#e6db74">&#39;value&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;scan&#39;</span>],
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Server-assigned; the install path recomputes all three.
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;checksum&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;created_epoch&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;updated_epoch&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>]);
</span></span></code></pre></div><p>Three things in there are doing real work. The <code>mode</code> is either <code>read_only</code> or <code>read_write</code>, and the engine enforces it, so a procedure that only reads can never quietly start writing underneath you. The <code>params</code> are typed against the same type vocabulary the columns use, each with a name, a <code>nullable</code> flag, and an optional default, which means the contract of the procedure is declared data and not a comment in a wiki. And the body is a list of steps whose values can reference each other, because a value in MongrelDB&rsquo;s procedure language is one of <code>literal</code>, <code>param</code>, <code>step_rows</code>, <code>step_row</code>, <code>step_scalar</code>, <code>object</code>, or <code>array</code>; the <code>param</code> kind dereferences a named call argument, and the <code>step_*</code> kinds consume the output of an earlier step, so a write procedure can read something in step one and feed it into a <code>put</code> or <code>upsert</code> in step two, all inside one committed call, with no client round trip in the middle.</p>
<h2 id="the-lifecycle-from-php">The lifecycle from PHP</h2>
<p>Once the definition exists, the rest of the lifecycle is four more calls and none of them involve SQL strings:</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\Database</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$db <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Database</span>(<span style="color:#e6db74">&#39;http://127.0.0.1:8090&#39;</span>, <span style="color:#a6e22e">token</span><span style="color:#f92672">:</span> $token);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Install or replace: POST /procedures with the definition from the section above.
</span></span></span><span style="display:flex;"><span>$created <span style="color:#f92672">=</span> $db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">createProcedure</span>($definition);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// List everything installed: GET /procedures.
</span></span></span><span style="display:flex;"><span>$names <span style="color:#f92672">=</span> <span style="color:#a6e22e">array_column</span>($db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">procedures</span>(), <span style="color:#e6db74">&#39;name&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Fetch one definition back, checksum and all.
</span></span></span><span style="display:flex;"><span>$def <span style="color:#f92672">=</span> $db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">procedure</span>(<span style="color:#e6db74">&#39;users_by_status&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Call it. Named arguments map onto the declared params.
</span></span></span><span style="display:flex;"><span>$rows <span style="color:#f92672">=</span> $db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">callProcedure</span>(<span style="color:#e6db74">&#39;users_by_status&#39;</span>, <span style="color:#a6e22e">args</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;status&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;active&#39;</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Retire it: DELETE /procedures/users_by_status.
</span></span></span><span style="display:flex;"><span>$db<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dropProcedure</span>(<span style="color:#e6db74">&#39;users_by_status&#39;</span>);
</span></span></code></pre></div><p>The call path is the piece PHP fits better than any client language I have used for this, because the server expects the call arguments as a JSON object keyed by param name and PHP&rsquo;s associative arrays are exactly that shape, so <code>$db-&gt;callProcedure('users_by_status', args: ['status' =&gt; 'active'])</code> sends <code>{&quot;status&quot;:&quot;active&quot;}</code> and each key dereferences the <code>param</code> of the same name in the body; the <code>args:</code> label at the call site is ordinary PHP 8 named-argument syntax on the method itself, but the real mapping runs through the array keys, which is worth saying precisely because <code>callProcedure('users_by_status', status: 'active')</code> would not work, and the habit of naming arguments that PHP developers picked up in 8.0 still lines up nicely with a contract where every param is declared with a name and a type on the server side. One implementation detail in the client is worth knowing because it explains a whole class of confusing bugs you will now never have: <code>callProcedure</code> casts the args array to an object before encoding, since an empty PHP array serializes as <code>[]</code>, a JSON sequence, while the server requires <code>{}</code>, a map, and that one cast is the difference between a procedure with no arguments working and the server rejecting your payload shape.</p>
<h2 id="what-steps-can-do">What steps can do</h2>
<p>The step vocabulary covers the operations the engine can commit atomically: <code>native_query</code> reads a table through conditions (<code>pk</code>, <code>bitmap_eq</code>, <code>bitmap_in</code>, <code>range</code>, <code>range_f64</code>, <code>is_null</code>, <code>is_not_null</code>, and <code>fm_contains</code> for full-text substring matching) with an optional projection and limit, and <code>put</code>, <code>upsert</code>, <code>delete_by_pk</code>, and <code>delete_rows</code> mutate, with <code>returning</code> on the writes when you want the rows back. Because every value in a step can be a <code>param</code> reference or a <code>step_scalar</code> from earlier in the list, the read-then-write pattern that usually takes an application-side transaction, select the id, compute something, insert the child row, compresses into one call:</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></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;kind&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;put&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;id&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;record_hit&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;table&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;counters&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;cells&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>        [<span style="color:#e6db74">&#39;column_id&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>, <span style="color:#e6db74">&#39;value&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;kind&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;param&#39;</span>, <span style="color:#e6db74">&#39;value&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;key&#39;</span>]],
</span></span><span style="display:flex;"><span>        [<span style="color:#e6db74">&#39;column_id&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">2</span>, <span style="color:#e6db74">&#39;value&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;kind&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;literal&#39;</span>, <span style="color:#e6db74">&#39;value&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;Int64&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>]]],
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;returning&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>Two value shapes in that block deserve a sentence of their own, because they look asymmetric until you know why they differ: a <code>literal</code> wraps one of the engine&rsquo;s typed values in a one-key tag, <code>['Int64' =&gt; 1]</code>, since the wire format has to know whether your <code>1</code> is an integer, a float, or a decimal128 before it can check the value against the column type, while a <code>param</code> value is a bare string because it carries no data at all and merely names the call argument to substitute at execution time.</p>
<p>Managing the catalog of procedures is a DDL operation, so installing, listing, describing, and dropping all require a principal with DDL permission, while calling one runs under the normal transaction discipline with the caller&rsquo;s table grants checked the same way any other operation would be; the procedure is not a way to escape permissions, it is a way to pre-arrange work the caller was already allowed to do.</p>
<h2 id="the-honest-tradeoff">The honest tradeoff</h2>
<p>The constraint to design around is that procedures called over the HTTP endpoint execute in the core engine, which does not host the SQL query layer, so a <code>sql_query</code> step is rejected there and your read steps are built from the native conditions instead; that is a deliberate boundary, because arbitrary SQL at call time is what the <code>/sql</code> endpoint is for, and the procedure surface stays limited to operations the engine can validate, checksum, and commit deterministically. The old world gave you unlimited power inside the procedure and paid for it with unreviewable string soup drifting in the catalog, the modern equivalent is a definition that is data, with a version you bump when the body changes and a checksum the server owns, and if you genuinely need free-form SQL in the middle of a procedure the honest answer is that MongrelDB has decided you do not, at least not over this endpoint, which is a smaller toolbox and, on the Fridays when the old catalog procedures used to break, a noticeably quieter one.</p>
]]></content:encoded></item></channel></rss>