<?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: Artur Piterov</title>
    <description>The latest articles on DEV Community by Artur Piterov (@piterovxyz).</description>
    <link>https://dev.to/piterovxyz</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%2F3948076%2F2bcb1530-96ee-4385-ba64-9c39cc4c1e06.jpg</url>
      <title>DEV Community: Artur Piterov</title>
      <link>https://dev.to/piterovxyz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/piterovxyz"/>
    <language>en</language>
    <item>
      <title>qrrot - database with AI</title>
      <dc:creator>Artur Piterov</dc:creator>
      <pubDate>Sat, 23 May 2026 18:05:35 +0000</pubDate>
      <link>https://dev.to/piterovxyz/qrrot-database-with-ai-8le</link>
      <guid>https://dev.to/piterovxyz/qrrot-database-with-ai-8le</guid>
      <description>&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%2F9lkj4xt134yf56otxbuj.jpg" 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%2F9lkj4xt134yf56otxbuj.jpg" alt="This is what working with AI looks like" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Writing your own in-memory database is a unique way to study Go under the hood and build a meaningful pet project. Creating a simple wrapper around a &lt;code&gt;map&lt;/code&gt; is boring. That's why I asked myself: what if I wrote a truly fast engine with binary storage, and bolted an interactive AI assistant on top of it, allowing you to communicate in natural language and making it execute chains of queries autonomously?&lt;/p&gt;

&lt;p&gt;Thus &lt;strong&gt;qrrot&lt;/strong&gt; was born — a Go-based in-memory store with a TCP interface, binary snapshots, and a built-in Gemini-based agent.&lt;/p&gt;

&lt;p&gt;In this article, I will provide the most detailed overview of my project: we'll break down the architecture, look at the benchmarks, explore how the AI works here, and at the end, I'll go over all the architectural pain points and flaws.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Under the hood: Data types and the engine
&lt;/h2&gt;

&lt;p&gt;At the core of qrrot lies a thread-safe &lt;code&gt;Store&lt;/code&gt; struct, protected by a &lt;code&gt;sync.RWMutex&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;   &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RWMutex&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike primitive string-string stores, the engine is strictly typed and supports three classic data types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;string&lt;/code&gt; - classic strings;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;int&lt;/code&gt; - 64-bit integers;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json&lt;/code&gt; - JSON objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All values are stored in memory in a &lt;code&gt;Value&lt;/code&gt; struct, which contains a byte slice and a type tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="kt"&gt;uint8&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TypeEmpty&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="n"&gt;TypeString&lt;/span&gt;
    &lt;span class="n"&gt;TypeInt&lt;/span&gt;
    &lt;span class="n"&gt;TypeJson&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;valueType&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;      &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easy to serialize data and avoid reflection overhead when serving it to the client. The commands are as familiar as possible: &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;del&lt;/code&gt;, &lt;code&gt;exists&lt;/code&gt;, &lt;code&gt;incr&lt;/code&gt;, &lt;code&gt;decr&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The battle for nanoseconds: The parser and network layer
&lt;/h2&gt;

&lt;p&gt;When you're writing a database, the "hottest" spot is parsing incoming commands. A regular &lt;code&gt;strings.Split&lt;/code&gt; won't work here: it allocates memory for every token, which will kill the garbage collector's performance at tens of thousands of requests per second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero-alloc (almost) parser
&lt;/h3&gt;

&lt;p&gt;I wrote a parser that iterates over a byte array, ignoring extra spaces and tabs. The real magic happens when handling strings with spaces (for example, JSON objects). The parser understands double quotes &lt;code&gt;"&lt;/code&gt; and escaped characters &lt;code&gt;\"&lt;/code&gt;, carefully collecting tokens into a pre-allocated &lt;code&gt;[8][]byte&lt;/code&gt; buffer.&lt;/p&gt;

&lt;p&gt;The benchmark results (Apple M4, Darwin ARM64) speak for themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw in-memory operations:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BenchmarkStore_Get&lt;/code&gt; - &lt;code&gt;6.29 ns/op&lt;/code&gt;, &lt;code&gt;0 B/op&lt;/code&gt;, &lt;code&gt;0 allocs/op&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BenchmarkStore_Put&lt;/code&gt; -&lt;code&gt;13.30 ns/op&lt;/code&gt;, &lt;code&gt;0 B/op&lt;/code&gt;, &lt;code&gt;0 allocs/op&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Command parsing:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BenchmarkParser_ParseGet&lt;/code&gt; - &lt;code&gt;25.70 ns/op&lt;/code&gt;, &lt;code&gt;32 B/op&lt;/code&gt;, &lt;code&gt;1 allocs/op&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BenchmarkParser_ParsePut&lt;/code&gt; -&lt;code&gt;51.19 ns/op&lt;/code&gt;, &lt;code&gt;32 B/op&lt;/code&gt;, &lt;code&gt;2 allocs/op&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Allocations in the parser are kept to an absolute minimum (1-2 per command) — they are only spent on converting bytes to a &lt;code&gt;string&lt;/code&gt; when creating the command object.&lt;/p&gt;

&lt;h3&gt;
  
  
  TCP server
&lt;/h3&gt;

&lt;p&gt;Network communication is built on the standard &lt;code&gt;net&lt;/code&gt; package. Each connection is handled in its own goroutine. The full cycle (receiving a packet -&amp;gt; parsing -&amp;gt; locking the mutex -&amp;gt; reading/writing -&amp;gt; responding to the client) works extremely fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BenchmarkTCPServer_Get&lt;/code&gt; - &lt;strong&gt;11 970 ns/op (~83 000 RPS)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BenchmarkTCPServer_Put&lt;/code&gt; - &lt;strong&gt;12 157 ns/op (~82 000 RPS)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. The killer feature: Interactive AI assistant
&lt;/h2&gt;

&lt;p&gt;To activate it, simply start the database with the &lt;code&gt;-ai&lt;/code&gt; flag and pass the &lt;code&gt;API_KEY&lt;/code&gt;. The &lt;code&gt;ai&lt;/code&gt; command then becomes available in the REPL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-step execution support
&lt;/h3&gt;

&lt;p&gt;Simply generating a single command doesn't work for complex tasks. For example, for the query &lt;em&gt;"if the user ivan exists, increment his age"&lt;/em&gt;, the AI cannot immediately issue a write command, as it doesn't know the state of the database. &lt;/p&gt;

&lt;p&gt;To solve this, a loop (up to 5 iterations) is implemented under the hood, where the AI communicates with the DB engine using special tags:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;QUERY READ: &amp;lt;command&amp;gt;&lt;/code&gt; - The AI asks the database to perform a read (e.g., &lt;code&gt;get ivan&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;QUERY WRITE: &amp;lt;command&amp;gt;&lt;/code&gt; - Intermediate data write.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RESULT READ:&lt;/code&gt; / &lt;code&gt;RESULT WRITE:&lt;/code&gt; - The final result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How it looks in practice:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You write: &lt;code&gt;ai delete ivan's profile if his age is less than 30&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The LLM answers the engine: &lt;code&gt;QUERY READ: all&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The engine transparently for you executes &lt;code&gt;all&lt;/code&gt;, searches for Ivan among the data, sees his age &lt;code&gt;{"age": 25} [json]&lt;/code&gt;, and sends it back to the LLM.&lt;/li&gt;
&lt;li&gt;The LLM understands that the user was found by the key (let's say, &lt;code&gt;ivan&lt;/code&gt;), and issues the final action: &lt;code&gt;RESULT WRITE: del ivan&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Human-in-the-loop (Protection against the machine uprising)
&lt;/h3&gt;

&lt;p&gt;The database &lt;strong&gt;never&lt;/strong&gt; blindly executes destructive AI commands. If the LLM generates write commands (put, del, incr, decr), qrrot pauses execution, draws a nice ASCII box with the execution plan, and waits for confirmation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai wants to execute the following write command(s):
┌─────────────────────────────────┐
     1. del ivan                                                                       
└─────────────────────────────────┘
execute final commands? (y/n): 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that neural network hallucinations won't destroy your prod.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Data on disk: Binary snapshots
&lt;/h2&gt;

&lt;p&gt;In-memory is cool, but data needs to be saved. Instead of using heavyweight formats (JSON/XML), qrrot saves dumps into its own custom binary format with the &lt;code&gt;QRRT&lt;/code&gt; signature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file structure is as dense as possible:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4 bytes signature (&lt;code&gt;QRRT&lt;/code&gt;) + 1 byte version.&lt;/li&gt;
&lt;li&gt;Then records follow sequentially: &lt;code&gt;[1 byte type] [2 bytes key length] [key] [4 bytes value length] [value]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Atomicity of saving:&lt;/strong&gt;&lt;br&gt;
When calling &lt;code&gt;exit&lt;/code&gt; or intercepting system signals (&lt;code&gt;SIGINT&lt;/code&gt;/&lt;code&gt;SIGTERM&lt;/code&gt;), the Graceful Shutdown mechanism is triggered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The dump is written to a temporary file &lt;code&gt;dump.qrr-*.tmp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;f.Sync()&lt;/code&gt; is called to force flushing OS buffers to the physical disk (protection against power outages).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os.Rename()&lt;/code&gt; is executed, which on POSIX systems is guaranteed to atomically replace the old file with the new one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;I/O Benchmarks (10 million keys with long values and JSON):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save to disk: &lt;strong&gt;2.41 seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Read, parse, and load 10M keys to RAM: &lt;strong&gt;3.14 seconds&lt;/strong&gt;.
Additionally, an OOM protection mechanism is implemented for reading corrupted files: if the dump specifies a value length greater than 16 MB, the database will refuse to load that key, preventing a system crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. On problems and architectural flaws
&lt;/h2&gt;

&lt;p&gt;Perfect code doesn't exist, especially if a project is written in two weeks by one person. qrrot was created as a pet project, and it is definitely not capable of competing with something like Redis in any way. It contains compromises that might shoot you in the foot as the load grows. Let's break them down:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Global lock
&lt;/h3&gt;

&lt;p&gt;The core of the database is a &lt;code&gt;Store&lt;/code&gt; under a single &lt;code&gt;sync.RWMutex&lt;/code&gt;. This is enough for 80k RPS on localhost, but on machines with dozens of cores, threads will start lining up in a queue.&lt;br&gt;
&lt;strong&gt;How to fix it:&lt;/strong&gt; Rewrite it using sharding. Split the store into an array of 256 segments (each with its own map and mutex). The segment is chosen by hashing the key. This will radically reduce lock contention.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OOM when taking snapshots
&lt;/h3&gt;

&lt;p&gt;In the current implementation, the &lt;code&gt;Snapshot&lt;/code&gt; method first calls &lt;code&gt;loadDataToRam()&lt;/code&gt;, which performs &lt;code&gt;maps.Copy(res, s.data)&lt;/code&gt;. &lt;br&gt;
This is a disaster for large databases. If you have 10 GB of data in memory, at the moment of creating a snapshot, the database will allocate another 10 GB of RAM just to create a safe copy of the map.&lt;br&gt;
&lt;strong&gt;How to fix it:&lt;/strong&gt; Either lock the map while writing to disk (kills DB availability), or implement MVCC (Multi-Version Concurrency Control) or a mechanism like RCU (Read-Copy-Update).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lack of a WAL (Write-Ahead Log)
&lt;/h3&gt;

&lt;p&gt;Snapshots are only saved on exit. If the server runs for a month, accumulates data, and the process is killed by &lt;code&gt;SIGKILL&lt;/code&gt; (or power is lost) — all data since the start will disappear.&lt;br&gt;
&lt;strong&gt;How to fix it:&lt;/strong&gt; Write an append-only log of every transaction to disk in real-time. On startup, the database should load the latest dump, and then "replay" operations from the WAL.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The AI assistant only works locally (in the REPL)
&lt;/h3&gt;

&lt;p&gt;The architecture of the AI agent with the interactive &lt;code&gt;y/n&lt;/code&gt; prompt is tied to &lt;code&gt;os.Stdin&lt;/code&gt; and &lt;code&gt;os.Stdout&lt;/code&gt;. If you start the DB in TCP server mode and send the &lt;code&gt;ai&lt;/code&gt; command over the network, the engine will honestly reply: &lt;code&gt;ai is only available in interactive console (i'll fix it)&lt;/code&gt;. To work over the network, an interactive protocol on top of TCP would need to be implemented.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Memory overhead for simple numbers
&lt;/h3&gt;

&lt;p&gt;Values of type &lt;code&gt;int&lt;/code&gt; (int64) take up 8 bytes, but qrrot packs them into a &lt;code&gt;Value&lt;/code&gt; struct with a type tag and a byte slice &lt;code&gt;[]byte&lt;/code&gt;. This generates unnecessary allocations and memory overhead. For small data, this is inefficient compared to &lt;code&gt;interface{}&lt;/code&gt; or unsafe tricks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;qrrot&lt;/strong&gt; is a great testing ground for experiments. The project proves that in pure Go, using only the standard library (excluding the AI client), you can quickly build a high-performance engine that withstands a massive RPS.&lt;/p&gt;

&lt;p&gt;The experience with an LLM as an autonomous agent turned out to be particularly interesting to me: the model handles multi-step tasks and JSON parsing inside the database perfectly, acting as a bridge between human language and strict DB logic.&lt;/p&gt;

&lt;p&gt;I'd be happy to hear your criticism, architectural advice, and see your pull requests: &lt;a href="https://github.com/piterovxyz/qrrot" rel="noopener noreferrer"&gt;https://github.com/piterovxyz/qrrot&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Thank you!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>go</category>
      <category>sql</category>
      <category>database</category>
    </item>
  </channel>
</rss>
