<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roberto Cornacchia</title>
    <description>The latest articles on DEV Community by Roberto Cornacchia (@swingbit).</description>
    <link>https://dev.to/swingbit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957258%2F0789d598-0e59-48ca-9e9a-4179ecf27a43.png</url>
      <title>DEV Community: Roberto Cornacchia</title>
      <link>https://dev.to/swingbit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swingbit"/>
    <language>en</language>
    <item>
      <title>Quack-Mate: a chess engine in SQL</title>
      <dc:creator>Roberto Cornacchia</dc:creator>
      <pubDate>Fri, 29 May 2026 17:26:57 +0000</pubDate>
      <link>https://dev.to/swingbit/quack-mate-a-chess-engine-in-sql-5dfc</link>
      <guid>https://dev.to/swingbit/quack-mate-a-chess-engine-in-sql-5dfc</guid>
      <description>&lt;p&gt;We have all seen SQL pushed past its comfort zone to build things like 3D renderers or cellular automata. Naturally, I wondered about a classic computer science challenge: could you write a functional chess engine purely inside an analytical database using standard relational algebra?&lt;/p&gt;

&lt;p&gt;The answer is yes. In fact, you can express the entire engine—generating moves, validating legality, scoring positions, and bubbling up the best strategy—inside a single &lt;code&gt;WITH RECURSIVE&lt;/code&gt; query. &lt;/p&gt;

&lt;h3&gt;
  
  
  One grand recursive CTE
&lt;/h3&gt;

&lt;p&gt;Recursive CTEs can handle a minimax tree search, just like the recursive minimax algorithm is implemented with imperative languages. By combining a top-down expansion of the board states with a bottom-up aggregation pass, DuckDB can execute a full minimax-based move search natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt;
    &lt;span class="c1"&gt;-- Expansion CTE: Top-down&lt;/span&gt;
    &lt;span class="n"&gt;search_tree&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c1"&gt;-- Expansion base case: Root Node&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_white_turn&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;

        &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

        &lt;span class="c1"&gt;-- Expansion step&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_white_turn&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;search_tree&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;possible_moves&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_DEPTH&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="c1"&gt;-- Minimax CTE: Bottom-up&lt;/span&gt;
    &lt;span class="n"&gt;minimax&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c1"&gt;-- Back-propagation base case:&lt;/span&gt;
        &lt;span class="c1"&gt;-- Target depth or terminal nodes&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;static_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;search_tree&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_DEPTH&lt;/span&gt;
           &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;search_tree&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

        &lt;span class="c1"&gt;-- Back-propagation step&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; 
            &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_white_turn&lt;/span&gt;
                 &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- White maximises&lt;/span&gt;
                 &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- Black minimises&lt;/span&gt;
            &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;minimax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;search_tree&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_DEPTH&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;recurring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minimax&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
        &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_white_turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;minimax&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query is obviously slightly simplified, but the structure is that of the actual production query. The production query, expanded with real move generations and all checks, is about 570 lines of SQL.&lt;/p&gt;

&lt;p&gt;It actually works. It plays legal, sound chess completely inside a single database transaction. &lt;/p&gt;

&lt;p&gt;But there is a massive catch. A recursive CTE query is inherently a Breadth-First Search (BFS). It has to hold every single board state of every single depth layer in memory simultaneously. Because it executes globally across rows, introducing classic Alpha-Beta pruning thresholds mid-query is practically impossible. &lt;/p&gt;

&lt;p&gt;If you try to look ahead past 3 plies (one and a half full turns), the combinatorial explosion takes over, and the query will instantly eat all your RAM. &lt;/p&gt;

&lt;h3&gt;
  
  
  Batched Principal Variation Search (BPVS)
&lt;/h3&gt;

&lt;p&gt;To make the engine actually playable with more than 3 plies without triggering an Out-Of-Memory crash, I had to transition from a single monolithic query to a hybrid architecture called &lt;strong&gt;Batched Principal Variation Search (BPVS)&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;All core calculations—board tracking, move validation, and position scoring—remain 100% encapsulated inside SQL queries executed by DuckDB. However, a thin external JavaScript loop acts as a coordinator to handle &lt;strong&gt;Iterative Deepening&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;The "Batched" part is the secret sauce that makes this viable in SQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pruning via Slices&lt;/strong&gt;: Sequential Alpha-Beta pruning checks moves one by one, which is too slow for database round-trips. Conversely, processing 40 moves at once prevents updating pruning thresholds mid-query. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive Chunking&lt;/strong&gt;: BPVS solves this by grouping generated moves into small, prioritised horizontal slices (Batch sizes of 1, 3, 8, then 64) using SQL Window Functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Thresholding&lt;/strong&gt;: The database evaluates a small batch at full depth to quickly establish a baseline threshold (PVS). The JavaScript coordinator reads this bound and dynamically injects it as a hardcoded literal straight into the &lt;code&gt;WHERE&lt;/code&gt; clause of the next SQL batch, allowing DuckDB to prune away thousands of hopeless branches before launching heavy relational joins.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Optimisation Stack
&lt;/h3&gt;

&lt;p&gt;To make this hybrid model performant enough for real-time play, I had to implement a complete stack of traditional chess engine heuristics adapted for relational tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vectorised Bitboards&lt;/strong&gt;: Storing the board state as a single row with 12 unsigned 64-bit integer columns (&lt;code&gt;UBIGINT&lt;/code&gt;), reducing piece movement to instant binary operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backwards Legality Probing&lt;/strong&gt;: Skipping complex pin masks entirely by applying moves in bulk, then running an &lt;code&gt;EXISTS&lt;/code&gt; subquery that traces enemy attacks backwards starting from the King's square.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move Ordering Heuristics&lt;/strong&gt;: Prioritising moves using custom weights for Transposition Table hits, MVV-LVA captures, Killer moves, and History tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggressive Selective Pruning&lt;/strong&gt;: Injecting Reverse Futility Pruning, Forward Futility Pruning, Late Move Pruning, and Late Move Reductions straight into query scans to slice off entire sub-trees early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quiescence Search&lt;/strong&gt;: Extending leaf nodes along active tactical capture lines to avoid the "horizon effect."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reality Check: How Does It Perform?
&lt;/h3&gt;

&lt;p&gt;Let's be clear: even with all these optimisations, it is significantly slower than a traditional imperative chess engine. A depth-4 search still takes a couple of seconds, and multi-threading offers little help because game-tree traversal is inherently sequential; adding more cores simply creates lock contention and thread coordination overhead inside DuckDB. &lt;/p&gt;

&lt;p&gt;This comes down to the &lt;strong&gt;Granularity Paradox&lt;/strong&gt;. In chess, pruning is far more important than raw throughput. The pruning requirement forces us to segment the search into many small queries, which is the opposite of what analytical databases like DuckDB are built for.&lt;/p&gt;

&lt;p&gt;But defeating Stockfish was never the point. The project proves that with the right indexing, data schema, and batched orchestration, an analytical engine can be bent to execute highly complex, deep algorithmic tree structures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22bpv719qv2glaozac9v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22bpv719qv2glaozac9v.png" alt="Quack-Mate" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Explore the Code and Play It
&lt;/h3&gt;

&lt;p&gt;The entire project is open-source and open for inspection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://swingbit.github.io/quackmate" rel="noopener noreferrer"&gt;Full Architecture Blog Post&lt;/a&gt;&lt;/strong&gt;: A deep-dive into architectural choices and execution profiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://swingbit.github.io/quack-mate/" rel="noopener noreferrer"&gt;Online Playable Engine&lt;/a&gt;&lt;/strong&gt;: Test your chess skills directly against Quack-Mate (desktop browser recommended).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/swingbit/quack-mate" rel="noopener noreferrer"&gt;Quack-Mate GitHub Repository&lt;/a&gt;&lt;/strong&gt;: Do you have bug fixes to suggest? Bring them on.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>chess</category>
      <category>duckdb</category>
      <category>showdev</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
