<?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: Gagandeep Singh Ahuja</title>
    <description>The latest articles on DEV Community by Gagandeep Singh Ahuja (@gagandeepahuja09).</description>
    <link>https://dev.to/gagandeepahuja09</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015303%2F8ec4b79c-d5de-469f-88e9-bb8554026427.jpg</url>
      <title>DEV Community: Gagandeep Singh Ahuja</title>
      <link>https://dev.to/gagandeepahuja09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gagandeepahuja09"/>
    <language>en</language>
    <item>
      <title>Building SaarDB, Part 7: SELECT Query</title>
      <dc:creator>Gagandeep Singh Ahuja</dc:creator>
      <pubDate>Sat, 15 Aug 2026 16:30:28 +0000</pubDate>
      <link>https://dev.to/gagandeepahuja09/building-saardb-part-7-select-query-3ama</link>
      <guid>https://dev.to/gagandeepahuja09/building-saardb-part-7-select-query-3ama</guid>
      <description>&lt;p&gt;In Part 6, we saw that &lt;code&gt;CREATE TABLE&lt;/code&gt; and &lt;code&gt;INSERT&lt;/code&gt; are just &lt;code&gt;PUT&lt;/code&gt; operations. Basis that, we can simply build &lt;code&gt;SELECT&lt;/code&gt; queries, just by modelling it as &lt;code&gt;GET&lt;/code&gt; operations, right? Well, not quite.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; queries can be complex with multiple joins, aggregations and multiple &lt;code&gt;WHERE&lt;/code&gt; conditions. Add to that, we can have multiple possible indexes to choose or even full-table scans if appropriate index is not present.&lt;/p&gt;

&lt;p&gt;We will not start with tackling all possible problems, but focus on solving the simplest set of problems. The simplest problem is primary-key based lookup queries as those are just &lt;code&gt;GET&lt;/code&gt; queries with key as the primary key itself.&lt;/p&gt;

&lt;p&gt;After solving for primary-key based lookup, we will move to full-table scan to fetch all rows of a table. This is the next important step, because it is a brute force approach which is usually taken when we don't have the required set of indexes. If we are able to do a full-table scan, we can still return result for any SELECT query in a specific table even if that might not be optimal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary Key Lookup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During insert, we had already set the following key: &lt;code&gt;_table:payments:5&lt;/code&gt; as id was the primary-key.&lt;br&gt;
After running the GET query, we just need to deserialise the schema based on the binary serialisation deserialisation contract we had agreed to while working on INSERT query. &lt;/p&gt;
&lt;h2&gt;
  
  
  Full-table Scan
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is a fundamentally different problem compared to SELECT by primary key. We need every row in the table, but:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We do not have a list of all the primary keys of a table anywhere. Our KV store only supports GET by exact key. How do we scan an entire table?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Full-table Scan Approach 1: Maintain a Separate Key List
&lt;/h2&gt;

&lt;p&gt;The first idea that comes to mind is to store a separate key &lt;code&gt;primary_keys:payments&lt;/code&gt; containing a regular list or a sorted list of all primary keys: &lt;code&gt;[1, 2, 3, 5, 7, ...]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On every INSERT, update this list. On SELECT *, read the list, then GET each key individually.&lt;/p&gt;

&lt;p&gt;This sounds simple but has multiple shortcomings. Let us walk through them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updating a sorted list is O(N).&lt;/strong&gt; To insert a new key into a sorted list of N keys, we need to find the insertion point and shift everything after it. With millions of rows, this becomes expensive for every single INSERT.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The list itself becomes a hot key.&lt;/strong&gt; Every INSERT to the &lt;code&gt;payments&lt;/code&gt; table writes to &lt;code&gt;primary_keys:payments&lt;/code&gt;. Under concurrent writes, this single key becomes a bottleneck. Every transaction needs a write lock on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not scale.&lt;/strong&gt; With 10 million rows, the list of primary keys alone is a massive value. Reading it into memory, deserializing it, then doing 10 million individual GETs is wasteful.&lt;/p&gt;

&lt;p&gt;This approach was actually explored during development and explicitly discarded. It is worth showing the wrong path as the right solution is much more elegant.&lt;/p&gt;
&lt;h2&gt;
  
  
  Full-table Scan Approach 2: Prefix Scan
&lt;/h2&gt;

&lt;p&gt;When we look at how the keys (rows) would be stored, we realise a clear pattern and a much more simpler, elegant and optimal solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_table:payments:1   → [row data]
_table:payments:2   → [row data]
_table:payments:5   → [row data]
_table:payments:7   → [row data]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that both memtable and SSTable are sorted. This means that all rows (keys) for payments table would already be adjacent to each other. &lt;/p&gt;

&lt;p&gt;We can just search for the first key which has the prefix as &lt;code&gt;_table:payments:&lt;/code&gt; and then carry out a sequential scan till we encounter a different key. This approach solves the two areas which made our previous approach inefficient:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We don't need to store any additional list of primary keys. The required data already exists sequentially adjacent in both SSTable and memtable.&lt;/li&gt;
&lt;li&gt;Since the data exists sequentially together, we get the performance benefit of sequential scans during reads instead of random scans. On the other hand, the previous approach required random scan for each GET query as the search for each key had to be done again explicitly even if we know that the keys are adjacent to each other.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We saw in the second blog that a key can reside either in memtable or in multiple SSTable files. And the priority order is memtable, then the SSTable files from newest to the oldest. Hence for carrying out a full-table scan, we require search through both memtable and SSTable.&lt;/p&gt;

&lt;p&gt;Let's explore how this would be done in both SSTable and memtable:&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefix Scan in SSTable
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Recalling the SSTable Structure
&lt;/h4&gt;

&lt;p&gt;In blog 2, we built SSTables with three parts: data blocks, an index block, and a footer block.&lt;/p&gt;

&lt;p&gt;Data blocks hold the actual key-value pairs in sorted order. The index block holds one entry per data block: the &lt;strong&gt;first key&lt;/strong&gt; of that data block and its byte offset. The footer holds the offset of the index block so we can find it.&lt;/p&gt;

&lt;p&gt;Let's say the payments table has 8 rows. The SSTable would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data block 0 (offset 0):
  _table:payments:1   → [row bytes]
  _table:payments:3   → [row bytes]
  _table:payments:5   → [row bytes]

Data block 1 (offset 512):
  _table:payments:10  → [row bytes]
  _table:payments:12  → [row bytes]
  _table:payments:15  → [row bytes]

Data block 2 (offset 1024):
  _table:payments:20  → [row bytes]
  _table:payments:25  → [row bytes]

Index block:
  _table:payments:1   → offset 0      (first key of block 0)
  _table:payments:10  → offset 512    (first key of block 1)
  _table:payments:20  → offset 1024   (first key of block 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index block is what makes search efficient. Instead of reading the entire file, we binary search the index to narrow down which data block to start from.&lt;/p&gt;

&lt;h4&gt;
  
  
  How GET Uses the Index Block
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;getLowerBound(target, indexBlock)&lt;/code&gt; returns the index of the &lt;strong&gt;largest key less than or equal to the target&lt;/strong&gt;. If all keys in the index are greater than the target, it returns -1.&lt;/p&gt;

&lt;p&gt;Let's dry run &lt;code&gt;GET _table:payments:12&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getLowerBound("_table:payments:12", index):

  _table:payments:1  ≤ _table:payments:12  → candidate
  _table:payments:10 ≤ _table:payments:12  → candidate (larger, preferred)
  _table:payments:20 &amp;gt; _table:payments:12  → too far

Returns index 1 → data block 1 (offset 512)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We jump to data block 1 and scan within it for the exact key. The logic is straightforward: since the index stores the &lt;strong&gt;first key of each block&lt;/strong&gt;, a key X must live in the block whose first key is ≤ X. The next block starts at something larger, so X cannot be there.&lt;/p&gt;

&lt;h4&gt;
  
  
  Prefix Scan: Same Function, One Edge Case
&lt;/h4&gt;

&lt;p&gt;For &lt;code&gt;SELECT * FROM payments&lt;/code&gt;, the prefix we are looking for is &lt;code&gt;_table:payments:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We apply the same getLowerBound on the prefix key to find the starting block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getLowerBound("_table:payments:", index):

  _table:payments:1  ≤ _table:payments:  ?
  _table:payments:10 ≤ _table:payments:  ?
  _table:payments:20 ≤ _table:payments:  ?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To answer this, we need to think about how string comparison works. Strings are compared character by character. &lt;code&gt;_table:payments:1&lt;/code&gt; and &lt;code&gt;_table:payments:&lt;/code&gt; are identical up to the last character. But &lt;code&gt;_table:payments:1&lt;/code&gt; has one more character after the colon. In string comparison, a longer string that extends a shorter one is always greater. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_table:payments:1  &amp;gt; _table:payments:
_table:payments:10 &amp;gt; _table:payments:
_table:payments:20 &amp;gt; _table:payments:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every index entry is greater than the prefix string. getLowerBound returns &lt;strong&gt;-1&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This will always be the case. &lt;code&gt;_table:payments:&lt;/code&gt; will always be lexicographically smaller than any actual row key like &lt;code&gt;_table:payments:1&lt;/code&gt; or &lt;code&gt;_table:payments:42&lt;/code&gt;, because the moment you append an ID after the colon, the key becomes strictly greater than the bare prefix string.&lt;/p&gt;

&lt;p&gt;So -1 does not mean "no matching rows." We need to look at the first key of the index to understand what -1 means in this context. There can be two possible situations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Situation 1: The file contains rows from the payments table.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index[0].key = _table:payments:1
HasPrefix(_table:payments:1, "_table:payments:") → YES
→ Start scanning from data block 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Situation 2: The file contains rows from a different table that sorts after payments.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index[0].key = _table:users:1
HasPrefix(_table:users:1, "_table:payments:") → NO
→ Skip this file entirely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hence, when getLowerBound returns -1, we need to check the prefix of the first key of the SSTable with the table prefix. If the prefix matches, all of the start rows in the SSTable are part of the same table. We should check sequentially till we encounter a different prefix. In case of a different prefix, we stop the search immediately.&lt;br&gt;
On the other, if the prefix of the first key of SSTable doesn't match the table prefix, we can skip the entire file as the table would come after the table that we are searching for.&lt;/p&gt;

&lt;p&gt;When getLowerBound returns a valid index (not -1), it means there are keys in the index that are ≤ the prefix. We start from that data block. &lt;/p&gt;

&lt;p&gt;This also handles the case where a data block contains rows from multiple tables. For example, if block 0 starts at &lt;code&gt;_table:cart:1&lt;/code&gt; but also contains &lt;code&gt;_table:payments:1&lt;/code&gt; through &lt;code&gt;_table:payments:9&lt;/code&gt;, getLowerBound returns block 0, ensuring we do not miss those rows. Starting from the last block whose first key is ≤ the prefix guarantees we never miss rows that happen to sit across a block boundary.&lt;/p&gt;
&lt;h4&gt;
  
  
  Sequential Scan: Reading Beyond One Data Block
&lt;/h4&gt;

&lt;p&gt;Once we have the starting data block, we read forward sequentially, collecting all keys that match the prefix. Unlike GET which stops at a single data block, prefix scan continues across multiple data blocks as long as the prefix keeps matching.&lt;/p&gt;

&lt;p&gt;We stop when we encounter a key whose first N characters (where N is the length of the prefix) are greater than the prefix. That means we have left the table's key space.&lt;/p&gt;

&lt;p&gt;Dry running the full scan with our example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;getLowerBound returns index 0 → start at data block 0

Data block 0:
  _table:payments:1  → has prefix → collect
  _table:payments:3  → has prefix → collect
  _table:payments:5  → has prefix → collect

Data block 1:
  _table:payments:10 → has prefix → collect
  _table:payments:12 → has prefix → collect
  _table:payments:15 → has prefix → collect

Data block 2:
  _table:payments:20 → has prefix → collect
  _table:payments:25 → has prefix → collect

End of file → done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the next key were &lt;code&gt;_table:products:1&lt;/code&gt;, the first 16 characters &lt;code&gt;_table:products:&lt;/code&gt; &amp;gt; &lt;code&gt;_table:payments:&lt;/code&gt;, so we stop immediately.&lt;/p&gt;

&lt;p&gt;One more difference from GET is that prefix scan must go through every SSTable file from newest to oldest. This is because different rows of the same table can exist across multiple files from separate flushes. Unlike GET, which stops at the first file containing the key, we cannot skip any file.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Code
&lt;/h4&gt;

&lt;p&gt;Based on our learnings, the core logic becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go through every SSTable file from newest to oldest.&lt;/li&gt;
&lt;li&gt;For each SSTable file, apply binary search on the index block array to find the &lt;code&gt;lowerBound&lt;/code&gt; for the prefix key. Prefix key is nothing but the common prefix which each table has. Example: &lt;code&gt;_table:payments:&lt;/code&gt;. Lower bound is the largest key in index block which is less than or equal to prefixKey. .&lt;/li&gt;
&lt;li&gt;If the &lt;code&gt;lowerBound&lt;/code&gt; is returned as -1, it could mean two things:

&lt;ul&gt;
&lt;li&gt;If the prefix of the first index block key in the SSTable file is greater than the prefix key of the table, it means that all prefixes in this file are greater and the file can be skipped.&lt;/li&gt;
&lt;li&gt;On the other hand, if prefix is exactly the same, we sequentially scan the file from the first data block itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Sequentially go through from the start of the data block ( as per the found &lt;code&gt;lowerBound&lt;/code&gt; data block) to the end of the data block (which is the start of the index block).

&lt;ul&gt;
&lt;li&gt;Maintain an in-memory hashmap of result where key is the prefix key and the value is the serialised row. This hashmap is required to ensure that a key is only added if it is seen for the first time. The newest occurence in the newest file is the source of truth for a key as it has the most up-to-date value.&lt;/li&gt;
&lt;li&gt;If we encounter a prefix which is greater than the table prefix key, we can exit and move to the next file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this intuition, the code reads naturally:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PrefixScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefixKey&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;tableMap&lt;/span&gt; &lt;span class="o"&gt;:=&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="c"&gt;// go through every SSTable file. rows of the same table can exist across multiple files&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;ssTableIndex&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexBlocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getLowerBound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefixKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// all index keys are greater than the prefix string.&lt;/span&gt;
            &lt;span class="c"&gt;// check the first key to understand why.&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefixKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c"&gt;// first key does not have our prefix. the file has no matching rows.&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c"&gt;// first key has our prefix. start from data block 0 and let&lt;/span&gt;
            &lt;span class="c"&gt;// the sequential scan stop when it moves past the prefix zone.&lt;/span&gt;
            &lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;endOffset&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexOffsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
        &lt;span class="n"&gt;tableMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequentiallyScanTableAndUpdateMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefixKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableMap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tableMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;sequentiallyScanTableAndUpdateMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableFile&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableKey&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;dataBlockStartOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fileEndOffset&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableMap&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ssTableDataBlockBuf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="n"&gt;fileEndOffset&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dataBlockStartOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ssTableFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableDataBlockBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataBlockStartOffset&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EOF&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableDataBlockBuf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;extractValueFromSsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableDataBlockBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;extractValueFromSsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableDataBlockBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// only set if not already found. newest file was processed first,&lt;/span&gt;
            &lt;span class="c"&gt;// so the first value we see for a key is always the most recent.&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tableMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;tableMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// stop as soon as we have moved past the prefix zone&lt;/span&gt;
            &lt;span class="n"&gt;keyPrefix&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keyPrefix&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tableKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tableMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tableMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prefix Scan In Memtable
&lt;/h3&gt;

&lt;p&gt;Since memtable contains most up-to date data, we would need to run a prefix scan on the memtable as well. &lt;br&gt;
The prefix scan is much simpler for memtable compared to SSTable. This is because in case of SSTable, we had to deal with "data block first-keys" within index block (means that index block stores the first keys of the data block and their offset) and due to that, we need to find the last key in index block which is just less than the prefix key (&lt;code&gt;lowerBound&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Lets take an example to make this more clear. Let's assume that the index block is:&lt;br&gt;
&lt;code&gt;_table:cart:4&lt;/code&gt;, &lt;code&gt;_table:cart:20&lt;/code&gt;, &lt;code&gt;_table:orders:10&lt;/code&gt;, &lt;code&gt;_table:payments:25&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If we are searching for payments table, we should be looking for keys from &lt;code&gt;_table:orders:10&lt;/code&gt; which is just less than or equal to &lt;code&gt;_table:payments&lt;/code&gt;, even though the only index block containing payments keyword is &lt;code&gt;_table:payments:25&lt;/code&gt;. This is because &lt;code&gt;_table:orders:10&lt;/code&gt; might contain some keys from &lt;code&gt;payments:1&lt;/code&gt; to &lt;code&gt;payments:24&lt;/code&gt; and we would miss out on those keys if we only search from  &lt;code&gt;_table:payments:25&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the other hand, in case of memtable we deal with all the list of keys present in the in-memory sorted hashmap. Hence, we just need to find the first key which is just greater than or equal to the prefix key (we can call this &lt;code&gt;upperBound&lt;/code&gt;). For example, in case of memtable, it will just return some key starting with &lt;code&gt;_table:payments&lt;/code&gt; like &lt;code&gt;_table:payments:4&lt;/code&gt; for payments table and we don't need to look at keys which are less than &lt;code&gt;_table:payments&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We have done exactly that in our memtable implementation. We have used &lt;code&gt;github.com/google/btree&lt;/code&gt; go package which implements B-tree and provides &lt;code&gt;AscendGreaterOrEqual&lt;/code&gt; function to return the first key greater than or equal to target and also allows iterating over those keys.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using Both
&lt;/h3&gt;

&lt;p&gt;Based on our learnings from blog 2, we now that memtable contains the most recent writes. Hence, in cases where the same key is present in both SSTable and Memtable, memtable is the source of truth.&lt;/p&gt;

&lt;p&gt;We have done exactly that in the implementation:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fullTableScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;memTableMap&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrefixScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ssTableMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrefixScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;scanOutput&lt;/span&gt; &lt;span class="o"&gt;:=&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="c"&gt;// Process memtable results first&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&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="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;memTableMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deserializeRowValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;scanOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scanOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Process SSTable results, skip keys already in memtable&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&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="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;ssTableMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;memTableMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// memtable has the most up-to-date value. if key already ready in memtable, skip&lt;/span&gt;
            &lt;span class="c"&gt;// reading SSTable.&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deserializeRowValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;scanOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scanOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scanOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Filtering by Non-Primary-Key Columns
&lt;/h2&gt;

&lt;p&gt;Let's look at filtering queries like:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; is not the primary key. We cannot do a targeted GET. There is no key &lt;code&gt;payments:pending&lt;/code&gt; in our KV store. The key is &lt;code&gt;payments:&amp;lt;id&amp;gt;&lt;/code&gt;, not &lt;code&gt;payments:&amp;lt;status&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without any additional structures, the only option is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Full table scan (prefix scan on &lt;code&gt;payments:&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For each row, deserialize and check if &lt;code&gt;status = pending&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Return matching rows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works. But it is O(N). We read every row in the table to find the ones that match. With 10 million rows and only 500 matching, 99.995% of the work is wasted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;We were able to build solution for running full-table scan which can also be extended to filter based on the required conditions.&lt;/p&gt;

&lt;p&gt;In Part 8, we will deep-dive into secondary and composite indexes and add support for them in our database. These would help optimise non-primary-key column queries to only go through the required number of rows.&lt;/p&gt;

&lt;p&gt;The code for SaarDB is available here:&lt;br&gt;
GitHub: &lt;a href="https://github.com/gagandeepahuja09/saardb" rel="noopener noreferrer"&gt;https://github.com/gagandeepahuja09/saardb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>software</category>
    </item>
    <item>
      <title>Building SaarDB, Part 6: How SQL Queries Become Key-Value Operations</title>
      <dc:creator>Gagandeep Singh Ahuja</dc:creator>
      <pubDate>Mon, 10 Aug 2026 03:15:41 +0000</pubDate>
      <link>https://dev.to/gagandeepahuja09/building-saardb-part-6-how-sql-queries-become-key-value-operations-mhf</link>
      <guid>https://dev.to/gagandeepahuja09/building-saardb-part-6-how-sql-queries-become-key-value-operations-mhf</guid>
      <description>&lt;p&gt;In Blog 5, we built a SQL parser. It can take this:&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and turn it into a struct:&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="n"&gt;InsertIntoTable&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ColumnValues&lt;/span&gt;&lt;span class="o"&gt;:&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="s"&gt;"500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"payment_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this is still not enough for the storage engine.&lt;/p&gt;

&lt;p&gt;Our storage engine only knows how to store key-value pairs. It does not know what a table is. It does not know what a column is. It does not know that &lt;code&gt;500&lt;/code&gt; is an integer, &lt;code&gt;pending&lt;/code&gt; is a string, and &lt;code&gt;1&lt;/code&gt; is a boolean.&lt;/p&gt;

&lt;p&gt;So, in this post we solve the missing bridge of persisting these in our key-value store.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CREATE and INSERT are PUT operations&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the first major realisation. A key-value store is extensible to store literally anything. This is what we have been saying from the first post itself. But now we will be taking actual examples to prove that.&lt;/p&gt;

&lt;h2&gt;
  
  
  CREATE TABLE Example
&lt;/h2&gt;

&lt;p&gt;Let's start with the create table example and see what should be the key and the value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serialisation
&lt;/h3&gt;

&lt;p&gt;The key should be something that uniquely identifies the table, which is straightforward enough in this case as the &lt;code&gt;table name&lt;/code&gt;. The value becomes everything else except the key, which is the schema of the table. &lt;/p&gt;

&lt;p&gt;So, in order to store the table name, we can append a reserved keyword as prefix like &lt;code&gt;schema&lt;/code&gt; as a unique identifier. The structure of the key becomes &lt;code&gt;_schema:&amp;lt;table_name&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The next question to answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we store a struct like below into our key value store where the value is always string?&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="n"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ColumnDetails&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"captured"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;PrimaryKeyColumnPosition&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One way is to serialise the entire struct into a string and store that directly. But in that case, deserialisation is a complex logic. JSON or struct serialisation and deserialisation is both space-heavy and compute intensive. This is because JSON and struct take up a lot of space, which is wasted space in case of a disk. They are good for external viewing purposes but not for internal use cases like this. Example: "ColumnDetails" is not needed to be stored and wasted space if we know that we need to write ColumnDetails first. Similar is the case for others fields like "ColumnName", "DataType" or "PrimaryKeyColumnPosition".&lt;/p&gt;

&lt;p&gt;If we store the data in such a way that we already know the structure beforehand and can have an agreed common serialisation and deserialisation strategy, it would save up a lot of space. Example: value should start with the position of primary key column, followed by N different column types and data types.&lt;/p&gt;

&lt;p&gt;Hence, the structure becomes something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[pkColumnPosition][columnDataType1][columnNameLength1][columnName1][columnDataType2][columnNameLength2][columnName2]...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the following here: we are following the same binary serialisation strategy which we have been doing since the first and second blog. &lt;code&gt;columnName&lt;/code&gt; is of type string (variable length), hence it is prefixed with length. On the other hand, &lt;code&gt;pkColumnPosition&lt;/code&gt;, &lt;code&gt;columnDataType&lt;/code&gt; are integers (fixed length), hence they are directly written. This also means that we will continue to write the data as array of bytes. In the end, it is bytes which is written to WAL or SS-Table Files.&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="c"&gt;// serialisation strategy: [PK_column_position][columnDataType1][columnNameLength1][columnName1][columnDataType2][columnNameLength2][columnName2]...&lt;/span&gt;
    &lt;span class="c"&gt;// secondary index serialisation is covered separately even though it is part of the same CREATE TABLE input.&lt;/span&gt;
    &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;serialiseCreateTableInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createTableInput&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="n"&gt;serialisedSchema&lt;/span&gt; &lt;span class="o"&gt;:=&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;span class="c"&gt;// 1. append primary key column position&lt;/span&gt;
        &lt;span class="n"&gt;serialisedSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serialisedSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createTableInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrimaryKeyColumnPosition&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;createTableInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnDetails&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c"&gt;// 2. append column data type (byte: 8 bit integer)&lt;/span&gt;
            &lt;span class="n"&gt;serialisedSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serialisedSchema&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;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="c"&gt;// 3. append length of column name&lt;/span&gt;
            &lt;span class="n"&gt;serialisedSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serialisedSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="c"&gt;// 4. append column name&lt;/span&gt;
            &lt;span class="n"&gt;serialisedSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serialisedSchema&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;serialisedSchema&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one more interesting thing in the above code. Notice the second comment where we append the data type. Data type appears to be a string. But due to the limited set of possible values of a column data type, we use a byte (8 bit integer) here allowing us to save space. For example: if we are only supporting 3 different datatypes: INT, STRING, BOOL, then all of them can be represented as unique integers: 0, 1, and 2. 8 bit integer means 2^8 (= 256) possible values which we can support.&lt;/p&gt;

&lt;p&gt;Once we have the key and the serialised value, this covers the write path. During the create table command, we just write this key value pair by utilising the &lt;code&gt;Put&lt;/code&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deserialisation
&lt;/h3&gt;

&lt;p&gt;In the earlier section, we discussed how the &lt;code&gt;CREATE TABLE&lt;/code&gt; can be modelled into a &lt;code&gt;Put&lt;/code&gt; operation by binary serialisation of the schema.&lt;/p&gt;

&lt;p&gt;While inserting the row using &lt;code&gt;INSERT&lt;/code&gt; command, we require knowing the schema of the table. This is because, within the AST struct we had represented every column value as string (500 was "500"). But while storing those values on disk, we require storing it as per the relevant data type.&lt;/p&gt;

&lt;p&gt;In order to fetch the schema, we require firing a GET query for the specific table name as the key. It would be wasteful to fire this GET query everytime an INSERT query needs to be performed. Hence, a series of GET queries for all the tables should be performed during the application bootup itself to store the schema of each table in-memory.&lt;/p&gt;

&lt;p&gt;Since, we already know the structure of create table value as below, we need to iterate through the byte array and read the required number of bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    [pkColumnPosition][columnDataType1][columnNameLength1][columnName1][columnDataType2][columnNameLength2][columnName2]...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below code demonstrates this logic. This deserialisation logic is run for each table during the application bootup:&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;func&lt;/span&gt; &lt;span class="n"&gt;deserialiseCreateTableInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&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;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;createTableMeta&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTable&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unexpected error while reading primary key column position"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// 1. read 4 bytes of pk column position. primary key column position is a 32-bit integer.&lt;/span&gt;
    &lt;span class="c"&gt;// iterate byte array by 4 (can be done within a byte itself if we set a limit of 255 columns in a table)&lt;/span&gt;
    &lt;span class="n"&gt;primaryKeyColumnPosition&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;createTableMeta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrimaryKeyColumnPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primaryKeyColumnPosition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

    &lt;span class="n"&gt;columnDetails&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;columnMeta&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;
        &lt;span class="n"&gt;dataType&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unexpected error while reading column data type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// 2. read 1 byte of data type and convert to internal enum&lt;/span&gt;
        &lt;span class="c"&gt;// iterate byte array by 1&lt;/span&gt;
        &lt;span class="n"&gt;columnMeta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unexpected error while reading column length"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// 3. read 4 bytes of column name length&lt;/span&gt;
        &lt;span class="c"&gt;// iterate byte array by 4&lt;/span&gt;
        &lt;span class="n"&gt;columnNameLength&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnNameLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unexpected error while reading column name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// 4. read column_name_length bytes to fetch column name&lt;/span&gt;
        &lt;span class="c"&gt;// iterate byte array by column_name_length&lt;/span&gt;
        &lt;span class="n"&gt;columnMeta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt; &lt;span class="o"&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnNameLength&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnNameLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;columnDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columnMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;createTableMeta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;columnDetails&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;createTableMeta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Atomicity in CREATE TABLE
&lt;/h3&gt;

&lt;p&gt;We are still missing one piece in the application bootup process. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If we require fetching all create table rows, how do we come to know of all of the tables?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A simple solution is to write all of the table names in a separate key called &lt;code&gt;_catalog:table_names&lt;/code&gt; with the value as comma separated list of all tables. This makes the Create Table operation non-atomic with multiple PUT commands (PUT for "table schema" and "table names").&lt;/p&gt;

&lt;p&gt;In order to make the CREATE TABLE atomic, we will utilise the transaction capability we built in blog 4. Our &lt;code&gt;CreateTable&lt;/code&gt; function would look something like:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;createTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createTableInput&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// 1. Transaction Begin&lt;/span&gt;
        &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tableNames&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tableNameVsSchemaMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tableNames&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableName&lt;/span&gt;
            &lt;span class="n"&gt;tableNames&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;tableNamesLength&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tableNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tableNames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;tableNamesLength&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c"&gt;// 2. PERFORM PUT operation with _catalog key and all table names.&lt;/span&gt;
        &lt;span class="c"&gt;// If PUT operation fails, rollback the transaction&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CatalogKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableNames&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transactionRolledback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;createTableInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableName&lt;/span&gt;

        &lt;span class="c"&gt;// 3. PERFORM PUT operation with _schema:[table_name] key&lt;/span&gt;
        &lt;span class="c"&gt;// If PUT operation fails, rollback the transaction&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SchemaTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableName&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;serialiseCreateTableInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createTableInput&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transactionRolledback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// 4. Transaction Commit&lt;/span&gt;
        &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  INSERT INTO Example
&lt;/h2&gt;

&lt;p&gt;We now have a fairly good understanding on how &lt;code&gt;CREATE TABLE&lt;/code&gt; converts AST struct into raw bytes. We will now carry out a similar deep-dive with the &lt;code&gt;INSERT INTO&lt;/code&gt; command.&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Serialisation
&lt;/h3&gt;

&lt;p&gt;Similar to CREATE TABLE case, we need to figure out what should be the key and the value. The &lt;code&gt;key&lt;/code&gt; choice is straightforward in this case as well. A primary key is the unique identifier for a row. Storing the primary key as the key for the key-value store also ensures that we don't require scanning the full table to find out the required key. We can just fire a &lt;code&gt;GET&lt;/code&gt; query with the specific key to fetch the specific row. So, the key can be something like &lt;code&gt;_table:&amp;lt;table_name&amp;gt;:&amp;lt;primary_key_value&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The value would contain all of the values. Example: &lt;code&gt;(500, payment_1, pending, 1)&lt;/code&gt; in this case. To reduce space, we can remove primary key from the value. For simplicity, we have not done that in the current version.&lt;/p&gt;

&lt;p&gt;In order to serialise the value, each column value is stored as per the column datatype. We utilise the schema saved in-memory during application bootup to check the schema instead of firing a GET query explicitly.&lt;/p&gt;

&lt;p&gt;As of now, we only support 3 data types: &lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;String&lt;/code&gt; and &lt;code&gt;Bool&lt;/code&gt;. Only string values are prefixed with length while &lt;code&gt;Int&lt;/code&gt; and &lt;code&gt;Bool&lt;/code&gt; don't require length prefix.&lt;/p&gt;

&lt;p&gt;So, the serialisation and deserialisation structure would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;_table:&amp;lt;table_name&amp;gt;:&amp;lt;primary_key_value&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;value1&lt;/span&gt;&lt;span class="pi"&gt;][&lt;/span&gt;&lt;span class="nv"&gt;size_of_value2&lt;/span&gt;&lt;span class="pi"&gt;][&lt;/span&gt;&lt;span class="nv"&gt;value2&lt;/span&gt;&lt;span class="pi"&gt;][&lt;/span&gt;&lt;span class="nv"&gt;value3&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: value1 and value3 are fixed sized datatype like int and bool while value2 is variable sized  datatype like string.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;serialiseInsertIntoTableInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;insertIntoTableInput&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InsertIntoTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;key&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;valueSchemaBuf&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;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;insertIntoTableInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableName&lt;/span&gt;
        &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tableNameVsSchemaMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;primaryKeyValue&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columnValue&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;insertIntoTableInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnValues&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrimaryKeyColumnPosition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;primaryKeyValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;columnValue&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnDetails&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;valueInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="c"&gt;// 1. Integer case: append 4 byte integer directly&lt;/span&gt;
                &lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueInt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="c"&gt;// 2. String case: first append length of the string which is 4 bytes and then append the string&lt;/span&gt;
                &lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnValue&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="n"&gt;columnValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sqlparser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bool&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="c"&gt;// 3. Boolean case: append 1 byte boolean directly&lt;/span&gt;
                &lt;span class="n"&gt;valueInt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;valueInt&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;valueInt&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"only 0 and 1 values supported for BOOL data type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueInt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_table:%s:%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primaryKeyValue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;valueSchemaBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Optimising Space
&lt;/h3&gt;

&lt;p&gt;When we implement more data types, we see the beauty on how they help optimise space of the row and in turn the entire table. A few datatype examples are:&lt;/p&gt;

&lt;h4&gt;
  
  
  VARCHAR(255)
&lt;/h4&gt;

&lt;p&gt;As of now, the string datatype that we added is taking up 32 bit or 4 bytes for storing length. We would have frequently seen the VARCHAR datatypes capped at a length of 255 characters. &lt;/p&gt;

&lt;p&gt;This number 255 is intentional (2^8 = 256). Keeping varchar limited till 255 ensures that a single byte is sufficient rather than requiring 4 bytes length prefix for an integer.&lt;/p&gt;

&lt;p&gt;For most use cases 255 characters are sufficient for representing the required string datatype. &lt;/p&gt;

&lt;h4&gt;
  
  
  CHAR(14)
&lt;/h4&gt;

&lt;p&gt;Apart from VARCHAR, we would have also seen constant character datatypes, &lt;code&gt;CHAR&lt;/code&gt; used while creating tables in MySQL or Postgres. They are especially useful for columns which require storing a unique id. &lt;br&gt;
These constant character datatypes help reduce space. This is because unlike regular string or  &lt;code&gt;VARCHAR&lt;/code&gt; datatypes, a prefix for indicating length is not required to be written, potentially saving 1 to 4 bytes of space.&lt;/p&gt;

&lt;h4&gt;
  
  
  TINYINT
&lt;/h4&gt;

&lt;p&gt;TINYINT is a datatype which takes up 1 byte of space. This means that it can take up 2^8 values from -128 to 127. If we use UNSIGNED, values from 0 to 255 can be supported. &lt;/p&gt;

&lt;p&gt;It can help save space from regular 4 bytes to 1 byte in tables where we know that the value will not exceed this range. Example: percentage or student marks or student ages.&lt;/p&gt;

&lt;h4&gt;
  
  
  ENUMs
&lt;/h4&gt;

&lt;p&gt;ENUMs are a very useful datatype in cases where we know that the column has low cardinality values. This just means that the number of unique possible values are quite low. &lt;/p&gt;

&lt;p&gt;As an example: payment status could only have a limited set of values. Instead of storing string every time, enums allow mapping each string to a unique number. For example: payment status "success" is mapped to number 0, "failed" is mapped to number 1, "captured" is mapped to number 2 and so on. &lt;/p&gt;

&lt;p&gt;This means that every time we just need to store a 1 byte number or a TINYINT instead of storing the actual strings where the actual string could take around 21 bytes of space for a 20 character string (1 byte for indicating length and 20 bytes for the actual string). &lt;/p&gt;

&lt;h3&gt;
  
  
  Atomicity in INSERT INTO
&lt;/h3&gt;

&lt;p&gt;Soon, similar to &lt;code&gt;CREATE TABLE&lt;/code&gt; command, we will require wrapping the &lt;code&gt;INSERT INTO&lt;/code&gt; command also in a transaction block to achieve atomicity. This is because secondary index updates require a separate &lt;code&gt;PUT&lt;/code&gt; operation. We will deep-dive more on this in a future blog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deserialisation
&lt;/h3&gt;

&lt;p&gt;Deserialisation of the value stored during &lt;code&gt;INSERT&lt;/code&gt; would be required during &lt;code&gt;SELECT&lt;/code&gt; query. &lt;/p&gt;

&lt;p&gt;We already covered a deep-dive on deserialisation in the &lt;code&gt;CREATE TABLE&lt;/code&gt; section by iterating through the byte array and reading bytes based on the common contract structure which is agreed during serialisation. The deserialisation implementation is hence kept out of the blog and can be tried out by the reader as an interesting exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;In blog 5 and 6, we got the intuition behind how &lt;code&gt;CREATE&lt;/code&gt; and &lt;code&gt;INSERT&lt;/code&gt; SQL queries can be converted into PUT operations within our existing key-value store. &lt;br&gt;
In the upcoming blogs, we shift the focus to the read path and supporting &lt;code&gt;SELECT&lt;/code&gt; queries. &lt;/p&gt;

&lt;p&gt;The code for SaarDB is available here:&lt;br&gt;
GitHub: &lt;a href="https://github.com/gagandeepahuja09/saardb" rel="noopener noreferrer"&gt;https://github.com/gagandeepahuja09/saardb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>go</category>
      <category>sql</category>
    </item>
    <item>
      <title>Building SaarDB, Part 5: SQL Parsing</title>
      <dc:creator>Gagandeep Singh Ahuja</dc:creator>
      <pubDate>Mon, 03 Aug 2026 03:37:48 +0000</pubDate>
      <link>https://dev.to/gagandeepahuja09/part-5-sql-parsing-turning-strings-into-commands-397m</link>
      <guid>https://dev.to/gagandeepahuja09/part-5-sql-parsing-turning-strings-into-commands-397m</guid>
      <description>&lt;p&gt;In Parts 1-4, we built a transactional key-value store. It has WAL for durability, memtables and SSTables for storage, compaction to control file growth, and transactions for atomic multi-key writes.&lt;/p&gt;

&lt;p&gt;Now we move to the query layer where users can actually fire SQL queries like:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt; &lt;span class="nb"&gt;BOOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payment_1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this blog and the next one we answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we translate SQL strings into operations our key-value store already understands?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post focuses on the first half of that bridge, which is parsing SQL into structured commands. In the next post, we will take those commands and turn &lt;code&gt;CREATE TABLE&lt;/code&gt; and &lt;code&gt;INSERT&lt;/code&gt; into bytes on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea: SQL Becomes Structured Data
&lt;/h2&gt;

&lt;p&gt;The storage engine does not understand SQL. It understands keys, values, WAL entries, memtables, SSTables, and transactions.&lt;/p&gt;

&lt;p&gt;So the SQL layer has two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse a human-readable SQL string into a structured object.&lt;/li&gt;
&lt;li&gt;Translate that structured object into key-value operations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE payments (...)
    -&amp;gt; CreateTable{TableName: "payments", ColumnDetails: ...}

INSERT INTO payments VALUES (...)
    -&amp;gt; InsertIntoTable{TableName: "payments", ColumnValues: ...}

SELECT * FROM payments WHERE id = payment_1
    -&amp;gt; SelectFromTable{TableName: "payments", QueryConditions: ...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we have these structs, the rest of the database can stop dealing with raw strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Parse SQL Directly in the DB Layer?
&lt;/h2&gt;

&lt;p&gt;Imagine if &lt;code&gt;db.CreateTable()&lt;/code&gt; directly walked through the SQL string and also updated storage. That would mix two very different responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parsing grammar,&lt;/li&gt;
&lt;li&gt;executing database operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping them separate makes the system easier to reason about. The parser validates syntax and builds an AST. The DB layer receives that AST and decides what to store.&lt;/p&gt;

&lt;p&gt;An AST, or Abstract Syntax Tree, is just a structured representation of the command.&lt;/p&gt;

&lt;p&gt;This is how the AST struct for the 3 common use cases, &lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;SELECT&lt;/code&gt;  would look like:&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;CreateTable&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;TableName&lt;/span&gt;                &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ColumnDetails&lt;/span&gt;            &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;
    &lt;span class="n"&gt;PrimaryKeyColumnPosition&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;SecondaryIndexes&lt;/span&gt;         &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;SecondaryIndex&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;InsertIntoTable&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;TableName&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ColumnValues&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SelectFromTable&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;TableName&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ColumnsRequired&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;QueryConditions&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;QueryCondition&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;InsertIntoTable.ColumnValues&lt;/code&gt; is still &lt;code&gt;[]string&lt;/code&gt;. The parser does not convert &lt;code&gt;500&lt;/code&gt; into an integer yet. That conversion can happen while storing in the database. The parser only understands syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing SQL Queries
&lt;/h2&gt;

&lt;p&gt;Let's take an example for parsing one of the SQL queries.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;CREATE TABLE&lt;/code&gt;, the expected shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE &amp;lt;table_name&amp;gt; (
    &amp;lt;column_name&amp;gt; &amp;lt;data_type&amp;gt;,
    ...
    PRIMARY KEY (&amp;lt;column_name&amp;gt;)
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to extract all of the useful properties out of the string into our AST &lt;code&gt;CreateTable&lt;/code&gt; struct. One way could be to go through each of the words and keep on extracting character by character and word by word.&lt;/p&gt;

&lt;p&gt;So, in case of &lt;code&gt;CREATE TABLE&lt;/code&gt; queries, the parser should do something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Expect keyword &lt;code&gt;CREATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Expect keyword &lt;code&gt;TABLE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Expect the table name. Store those in the AST struct.&lt;/li&gt;
&lt;li&gt;Expect &lt;code&gt;(&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read column definitions until &lt;code&gt;)&lt;/code&gt;. While reading column definitions, extract the column name and the data type. Store those in the AST struct.&lt;/li&gt;
&lt;li&gt;If it sees &lt;code&gt;PRIMARY KEY (...)&lt;/code&gt;, store the primary-key column.&lt;/li&gt;
&lt;li&gt;Return a &lt;code&gt;CreateTable&lt;/code&gt; struct.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The SQL query could also contain whitespaces (spaces, tabs '\t' and new lines '\n'). The SQL query should also end with a &lt;code&gt;;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice that the &lt;code&gt;CREATE TABLE&lt;/code&gt; query parsing requires identifying a lot of details which are specific to this query like table name, column names and data type and the primary key column name, along with the necessary validations.&lt;/p&gt;

&lt;p&gt;Apart from these distinct requirements for the specific query, there is a lot of common logic that needs to be handled for each SQL query. This includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each query includes &lt;code&gt;keywords&lt;/code&gt; like &lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;TABLE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;INTO&lt;/code&gt;, &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt; and &lt;code&gt;AND&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each query includes one or more of expressions like &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, and &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each query can contain unique &lt;code&gt;identifiers&lt;/code&gt; based on the use case. In case of CREATE TABLE queries, this could be table name, column name, data type or primary key column name. Similarly in case of SELECT query, it could be a column name or its query condition. &lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Tokeniser: Breaking SQL Into Small Pieces
&lt;/h3&gt;

&lt;p&gt;Tokenisation is a common operation applicable across all SQL queries. We break the input string into small meaningful pieces called tokens. &lt;/p&gt;

&lt;p&gt;For this query:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tokens are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE     keyword
TABLE      keyword
payments   identifier
(          symbol
amount     identifier
INT        identifier
,          symbol
id         identifier
STRING     identifier
)          symbol
;          symbol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tokenisation is the first step performed to avoid directly parsing the string. We will soon see how this step drastically simplifies things because of us working with tokens and not raw characters.&lt;/p&gt;

&lt;p&gt;We will build a function whose job is to find the next token from the input string. It tracks the index position in the input string in a private variable &lt;code&gt;pos&lt;/code&gt; which indicates the index till which we have already tokenised.&lt;/p&gt;

&lt;p&gt;The tokeniser walks through the input character by character:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Skip whitespace.&lt;/li&gt;
&lt;li&gt;Return symbols like &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;)&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, and &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Return conditional operators like &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;gt;=&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read alphanumeric words and decide whether they are keywords or identifiers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how the &lt;code&gt;NextToken&lt;/code&gt; function looks like:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Tokeniser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;NextToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skipWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// the entire input string is read&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EOF&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c"&gt;// SYMBOL token&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'('&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;')'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;','&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;';'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'*'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYMBOL&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// CONDITIONAL_OPERATOR token &lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContainsRune&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conditionalOperators&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContainsRune&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conditionalOperators&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CONDITIONAL_OPERATOR&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;isAlphanumeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// KEYWORD or IDENTIFIER token&lt;/span&gt;
    &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;keywordsMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToUpper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEYWORD&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToUpper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIER&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;word&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keywordsMap&lt;/code&gt; would contain the list of all supported SQL keywords like &lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;SELECT&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;Note that the only job of &lt;code&gt;NextToken&lt;/code&gt; is to find the next upcoming token and classify it as a &lt;code&gt;KEYWORD&lt;/code&gt;, &lt;code&gt;IDENTIFIER&lt;/code&gt;, &lt;code&gt;SYMBOL&lt;/code&gt; or &lt;code&gt;CONDITIONAL_OPERATOR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a common step required for parsing each SQL query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consuming Tokens
&lt;/h3&gt;

&lt;p&gt;Once we know the &lt;code&gt;NextToken&lt;/code&gt;, consuming, validating the token and converting into AST struct becomes much simpler.&lt;/p&gt;

&lt;p&gt;We also have a common &lt;code&gt;consume&lt;/code&gt; function which is used by each of the parser implementation functions (&lt;code&gt;ParseCreateTable&lt;/code&gt;, &lt;code&gt;ParseInsertInto&lt;/code&gt;, &lt;code&gt;ParseSelect&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;While parsing the input query, we always know what is the expected token type (&lt;code&gt;KEYWORD&lt;/code&gt; or &lt;code&gt;SYMBOL&lt;/code&gt; or &lt;code&gt;IDENTIFIER&lt;/code&gt;). In case of token type as &lt;code&gt;KEYWORD&lt;/code&gt; or &lt;code&gt;SYMBOL&lt;/code&gt;, we also know what is the expected value. For example: In case of CREATE TABLE, the first two expected keywords are &lt;code&gt;CREATE&lt;/code&gt; and &lt;code&gt;TABLE&lt;/code&gt;. After that we expect the table name and just after that we expect the symbol &lt;code&gt;(&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is how the consume function looks like.&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;TokenType&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;

&lt;span class="c"&gt;// Supported Token Types&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;IDENTIFIER&lt;/span&gt;           &lt;span class="n"&gt;TokenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"IDENTIFIER"&lt;/span&gt;
    &lt;span class="n"&gt;KEYWORD&lt;/span&gt;              &lt;span class="n"&gt;TokenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"KEYWORD"&lt;/span&gt;
    &lt;span class="n"&gt;SYMBOL&lt;/span&gt;               &lt;span class="n"&gt;TokenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SYMBOL"&lt;/span&gt;
    &lt;span class="n"&gt;CONDITIONAL_OPERATOR&lt;/span&gt; &lt;span class="n"&gt;TokenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CONDITIONAL_OPERATOR"&lt;/span&gt;
    &lt;span class="n"&gt;EOF&lt;/span&gt;                  &lt;span class="n"&gt;TokenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"EOF"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="n"&gt;TokenType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedVal&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedVal&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&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;expectedVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"syntax error: expected %s %q, got %s %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedVal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&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;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokeniser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NextToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that in case of identifier, we would not be having any expected value (&lt;code&gt;expectedVal = ""&lt;/code&gt;) as identifiers are used to fill in the AST structure. While in case of keyword or symbol, we know the expected value (&lt;code&gt;expectedVal != ""&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Parsing CREATE TABLE
&lt;/h3&gt;

&lt;p&gt;Let's take CREATE TABLE as an example to solidify our understanding. Rest of the queries would have similar logic for parsing queries. We can utilise the &lt;code&gt;consume&lt;/code&gt; function for parsing any SQL query.&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt; &lt;span class="nb"&gt;BOOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The algorithm would look something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consume &lt;code&gt;CREATE&lt;/code&gt; as a &lt;code&gt;KEYWORD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Consume &lt;code&gt;TABLE&lt;/code&gt; as a &lt;code&gt;KEYWORD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Consume &lt;code&gt;IDENTIFIER&lt;/code&gt; for table name and set that within the AST struct of &lt;code&gt;CreateTable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Consume &lt;code&gt;(&lt;/code&gt; as a &lt;code&gt;SYMBOL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run a for loop till we encounter &lt;code&gt;)&lt;/code&gt; &lt;code&gt;SYMBOL&lt;/code&gt; as the current token.

&lt;ul&gt;
&lt;li&gt;Consume identifier for column name and data type and set that within the AST struct of &lt;code&gt;CreateTable&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The next token could be the keyword &lt;code&gt;PRIMARY&lt;/code&gt;. If yes, 

&lt;ul&gt;
&lt;li&gt;Consume keywords &lt;code&gt;PRIMARY&lt;/code&gt; and &lt;code&gt;KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Consume identifier for primary key column and set that within the AST struct of &lt;code&gt;CreateTable&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Consume symbol &lt;code&gt;,&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What we wrote can be modeled into code as:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ParseCreateTable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEYWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KeywordCreate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEYWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KeywordTable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDENTIFIER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SYMBOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SymbolOpenRoundBracket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;columnDetails&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;pkColumn&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&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;SymbolClosedRoundBracket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SYMBOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&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;KeywordPrimary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
                &lt;span class="n"&gt;pkColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parsePrimaryKeyColumn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;columnName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDENTIFIER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;columnType&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDENTIFIER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getDataTypeFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;columnDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;columnName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnDetails&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expected atleast one column detail, found none"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// current simplification that if PRIMARY KEY is not provided, consider first column as primary key&lt;/span&gt;
        &lt;span class="n"&gt;pkColumnPosition&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pkColumn&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;pkColumnPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;columnDetails&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pkColumn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;pkColumnPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pkColumnPosition&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"primary key column '%s' not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkColumn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SYMBOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SymbolClosedRoundBracket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ColumnDetails&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;columnDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;PrimaryKeyColumnPosition&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pkColumnPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser returns:&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="n"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ColumnDetails&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"captured"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;PrimaryKeyColumnPosition&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c"&gt;// 1 is the index of "id" in the ColumnDetails array, which is the primary key&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is already much easier for the DB layer to use than a raw SQL string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Parser Limitations
&lt;/h2&gt;

&lt;p&gt;This parser is intentionally small. A few examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;String literals are not quoted yet, so examples use &lt;code&gt;pending&lt;/code&gt;, not &lt;code&gt;'pending'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INSERT&lt;/code&gt; requires values for all columns in schema order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT&lt;/code&gt; supports simple &lt;code&gt;WHERE&lt;/code&gt; conditions and &lt;code&gt;AND&lt;/code&gt;, not joins, grouping, or expressions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE&lt;/code&gt; supports limited set of datatypes. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These limitations are acceptable for now because the goal is to build intuition one layer at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Parsing turns SQL strings into structs. But a struct is still not durable data.&lt;/p&gt;

&lt;p&gt;Now we need to answer the deeper question in the next post:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once we have &lt;code&gt;CreateTable&lt;/code&gt; and &lt;code&gt;InsertIntoTable&lt;/code&gt; AST structs, how do we store them inside a key-value engine?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The code for SaarDB is available here:&lt;br&gt;
GitHub: &lt;a href="https://github.com/gagandeepahuja09/saardb" rel="noopener noreferrer"&gt;https://github.com/gagandeepahuja09/saardb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Building SaarDB, Part 4: Transactions</title>
      <dc:creator>Gagandeep Singh Ahuja</dc:creator>
      <pubDate>Sun, 26 Jul 2026 22:21:55 +0000</pubDate>
      <link>https://dev.to/gagandeepahuja09/building-saardb-part-4-transactions-2af0</link>
      <guid>https://dev.to/gagandeepahuja09/building-saardb-part-4-transactions-2af0</guid>
      <description>&lt;p&gt;In Parts 1-3, we built a storage engine: WAL, memtable, SSTables, and compaction. Every operation is a single GET or PUT.&lt;/p&gt;

&lt;p&gt;But real applications rarely need just one write at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem: Multiple Writes Need to Be Atomic
&lt;/h2&gt;

&lt;p&gt;Consider a bank transfer: move $100 from account A to account B.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT account_A 400    (was 500, debit 100)
PUT account_B 600    (was 500, credit 100)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine the process crashes between these two writes. The first PUT succeeds, account A has $400. The second PUT never happens, account B still has $500.&lt;/p&gt;

&lt;p&gt;$100 has vanished. The system is in an inconsistent state.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In our current system, each PUT is independent. There is no way to say: "these two writes must both succeed or both fail."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the &lt;strong&gt;atomicity&lt;/strong&gt; problem. We need a way to group multiple writes into a single logical operation, a &lt;strong&gt;transaction&lt;/strong&gt; that either commits entirely or has no effect at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem: Concurrent Users Can Mess Each Other Up
&lt;/h2&gt;

&lt;p&gt;Atomicity is not the only issue. When multiple transactions run concurrently, they can interfere with each other in subtle ways. This is the &lt;strong&gt;isolation&lt;/strong&gt; problem. Let us walk through three concrete scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: Dirty (Uncommitted) Read
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time    T1                          T2
─────────────────────────────────────────────
1       PUT balance 100
2                                   GET balance → 100
3       ROLLBACK (undo PUT)
4                                   // T2 now holds value 100
                                    // but that value never existed!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;T2 read a value that T1 wrote but then rolled back. T2 is now making decisions based on data that was never committed. From the database's perspective it is data that never existed.&lt;/p&gt;

&lt;p&gt;Hence, a transaction should only be reading committed values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Lost Update
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time    T1                          T2
─────────────────────────────────────────────
1       GET balance → 50
2                                   GET balance → 50
3       balance = 50 + 20 = 70
4       PUT balance 70
5                                   balance = 50 + 30 = 80
6                                   PUT balance 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's say we solve for dirty reads by only reading committed values. In that case, we run into other issues.&lt;/p&gt;

&lt;p&gt;Let's say T1 adds $20. T2 adds $30. The correct final balance should be $100 (50 + 20 + 30). But the actual result is $80. T1's update is silently overwritten by T2 (hence the name lost update) because T2 read the old value before T1's write was visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: Write Skew
&lt;/h3&gt;

&lt;p&gt;A more subtle problem. Suppose we have two variables with a constraint: &lt;code&gt;x + y &amp;gt;= 0&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial state: x = 5, y = 5

Time    T1                          T2
─────────────────────────────────────────────
1       READ x → 5, READ y → 5
2                                   READ x → 5, READ y → 5
3       Check: x + y = 10 &amp;gt;= 0 ✓
4       SET x = -5
5                                   Check: x + y = 10 &amp;gt;= 0 ✓
6                                   SET y = -5
7       COMMIT
8                                   COMMIT

Result: x = -5, y = -5, x + y = -10 (VIOLATES constraint!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each transaction individually validated the constraint. But their combined effect violates it. Neither transaction saw the other's write because they both read before either wrote.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;write skew&lt;/strong&gt;. Each transaction makes a decision based on a snapshot that becomes stale by the time it commits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lost update and write skew look similar but the distinction matters: in a lost update, two transactions race on the &lt;strong&gt;same key&lt;/strong&gt;. &lt;br&gt;
In write skew, no single key is contested, T1 writes &lt;code&gt;x&lt;/code&gt;, T2 writes &lt;code&gt;y&lt;/code&gt;, but their combined effect violates a constraint that spans both keys.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Isolation Levels Exist?
&lt;/h2&gt;

&lt;p&gt;To solve these isolation problems (or anomalies), SQL databases define four isolation levels. Each level preventing more anomalies than the one before it. But with increasing isolation level, the concurrency and hence the overall througput can drop. This is because, in order to solve for these isolation anomalies, we can require acquiring more locks. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Dirty Reads&lt;/th&gt;
&lt;th&gt;Lost Updates&lt;/th&gt;
&lt;th&gt;Write Skew&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Uncommitted&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Serializable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prevented&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prevented&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prevented&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this blog, we will focus on &lt;strong&gt;Serializable&lt;/strong&gt;, the strongest isolation level. The result of concurrent transactions is equivalent to SOME serial (one-at-a-time) execution order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Approach: Two-Phase Locking (2PL)
&lt;/h2&gt;

&lt;p&gt;Though there are multiple ways of implementing serializable isolation, we will focus on the simplest approach for the current blog called two-phase locking. Though this approach also leads to the least level of concurrency and throughput.&lt;/p&gt;

&lt;p&gt;Before a transaction reads or writes a key, it must acquire a lock on that key. Other transactions that want to access the same key have to wait (or fail).&lt;/p&gt;

&lt;p&gt;Let's see how locks solve the lost update problem from earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time    T1                          T2
─────────────────────────────────────────────
1       LOCK balance
2       GET balance → 50
3                                   LOCK balance → blocked (T1 holds it)
4       PUT balance 70
5       UNLOCK balance
6                                   (unblocked)
7                                   GET balance → 70
8                                   PUT balance 100
9                                   UNLOCK balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;T2 cannot read &lt;code&gt;balance&lt;/code&gt; until T1 is done. No lost update.&lt;/p&gt;

&lt;p&gt;But when should a transaction release its locks? What if T1 unlocks &lt;code&gt;balance&lt;/code&gt; and then needs to access another key?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time    T1                          T2
─────────────────────────────────────────────
1       LOCK x
2       GET x → 5
3       UNLOCK x
4                                   LOCK x
5                                   PUT x = -5
6                                   UNLOCK x
7       LOCK y
8       GET y → 5
9       PUT y = -5
10      UNLOCK y

Result: x = -5, y = -5 (write skew!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;T1 released &lt;code&gt;x&lt;/code&gt; before the end of the transaction and in this case immediately after GET. T2 snuck in and wrote to &lt;code&gt;x&lt;/code&gt; before T1 was done. T1 then read &lt;code&gt;y&lt;/code&gt; and since T1 has stale value of x = 5, the &lt;code&gt;x + y &amp;gt;= 0&lt;/code&gt; still applied. Hence, even after applying locks, we still faced the write skew problem.&lt;/p&gt;

&lt;p&gt;The solution is to &lt;strong&gt;hold all locks until commit.&lt;/strong&gt; This is the core distinction and the purpose of a transaction. Locks are held till the transaction is complete. Lock is released only when we commit or abort a transaction. If T1 never releases &lt;code&gt;x&lt;/code&gt; until it is done with everything, T2 cannot sneak in.&lt;/p&gt;

&lt;p&gt;This is where the Two-Phase Locking name comes from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1: Growing or acquiring phase&lt;/strong&gt; (BEGIN to COMMIT): acquire locks as needed. Never release any.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2: Shrinking or releasing phase&lt;/strong&gt; (COMMIT or ROLLBACK): release all locks at once.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Read And Write Locks In A Transaction
&lt;/h2&gt;

&lt;p&gt;We talked about the need of read and write locks in the last blog from a process or goroutine perspective. Now, we will be applying the concepts of read and write locks within a transaction as well.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET&lt;/code&gt; command requires acquiring a read lock on a key while &lt;code&gt;PUT&lt;/code&gt; command requires acquiring a write lock.&lt;/p&gt;

&lt;p&gt;Similar to what we studied in the last blog, multiple transactions can hold read locks on the same key simultaneously. But only one transaction can hold write lock on a key at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking Lock State
&lt;/h2&gt;

&lt;p&gt;When a transaction wants to acquire a lock on a key, we need to answer: has some other transaction already acquired a lock on this key? And if so, is it a read lock or a write lock?&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracking locks per key
&lt;/h3&gt;

&lt;p&gt;While performing &lt;code&gt;PUT&lt;/code&gt; or &lt;code&gt;GET&lt;/code&gt; operation within a transaction, for each key, we need to know: which transactions currently hold a lock on it, and is it a read lock or a write lock?&lt;/p&gt;

&lt;p&gt;A map from key to lock state is the natural fit. And as we discussed above and in last blog, we know that a key can either have multiple readers or a single writer, never both. So the value struct needs to track both cases.&lt;/p&gt;

&lt;p&gt;In the code snippet below, &lt;code&gt;keyVsLocksAcquiredMap&lt;/code&gt; is the map to identify the write lock or read locks acquired by different transactions for a specific key.&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="n"&gt;keyVsLocksAcquiredMap&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;LocksAcquired&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;LocksAcquired&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;readerTxnIds&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;writerTxnId&lt;/span&gt;  &lt;span class="kt"&gt;uint64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Identifying transactions
&lt;/h3&gt;

&lt;p&gt;The above struct uses transaction IDs to identify who holds what. Each transaction gets a unique ID via an auto-incrementing counter assigned at &lt;code&gt;BEGIN&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;Transaction&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;id&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Efficiently releasing locks
&lt;/h3&gt;

&lt;p&gt;When a transaction commits or rolls back, we need to release all its locks. If we only had &lt;code&gt;keyVsLocksAcquiredMap&lt;/code&gt;, we would have to scan the entire map to find which keys this transaction locked. That is O(total keys in the system).&lt;/p&gt;

&lt;p&gt;Hence apart from a shared global &lt;code&gt;keyVsLocksAcquiredMap&lt;/code&gt;, each transaction maintains its own list of locked keys. This list would be part of the &lt;code&gt;Transaction&lt;/code&gt; struct.&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="n"&gt;lockAcquiredKeys&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Transaction&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;id&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;lockAcquiredKeys&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At release time, we iterate over this list and do O(1) lookups in &lt;code&gt;keyVsLocksAcquiredMap&lt;/code&gt; to clear each lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on durability
&lt;/h3&gt;

&lt;p&gt;None of these data structures need to be persisted to disk. Lock state and in-flight transaction metadata are purely in-memory. If the process crashes mid-transaction, the transaction was never committed, so there is nothing to recover. Only committed transactions are durable (via the WAL entry we write at commit time).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock Upgrades
&lt;/h2&gt;

&lt;p&gt;Consider a bank transfer. The transaction first reads the balance to check if there are sufficient funds, then writes the new balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T1: GET balance → 50   (acquires read lock)
// application logic: 50 + 20 = 70, sufficient funds ✓
T1: PUT balance 70     (needs write lock)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But T1 already holds a read lock on &lt;code&gt;balance&lt;/code&gt;. It now needs a write lock on the same key. Can we just upgrade the lock from read to write?&lt;/p&gt;

&lt;p&gt;This depends on who else is reading. Suppose T2 also holds a read lock on &lt;code&gt;balance&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T1: holds read lock on balance
T2: holds read lock on balance

T1 wants to upgrade to write lock...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we allow the upgrade, T1 can now write a new value while T2 is still reading the old one. T2 would make decisions based on a value that no longer exists. This is exactly the dirty read problem we are trying to prevent.&lt;/p&gt;

&lt;p&gt;So the rule for &lt;strong&gt;lock upgrades&lt;/strong&gt; is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If T1 is the &lt;strong&gt;only&lt;/strong&gt; reader, it can safely upgrade. No one else is reading the old value.&lt;/li&gt;
&lt;li&gt;If &lt;strong&gt;another transaction&lt;/strong&gt; also holds a read lock, T1 cannot upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our implementation, if an upgrade is not safe, we return an error immediately rather than waiting. Simple, but it means the caller must retry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Do Uncommitted Writes Go?
&lt;/h3&gt;

&lt;p&gt;Let's trace what happens during a transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T1: BEGIN
T1: PUT balance 70
T1: GET balance → 70
T1: PUT account_B 600
// ... more operations ...
T1: COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Between &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt;, the writes are not yet final. The transaction might still roll back. So where do these writes go?&lt;/p&gt;

&lt;p&gt;They cannot go to the WAL. If they did and the transaction rolls back, the WAL would contain data that should never have existed. On recovery, those writes would be replayed as if they were committed.&lt;/p&gt;

&lt;p&gt;They cannot go to the memtable either. If they did, other transactions running concurrently could read the uncommitted values. This is the dirty read problem.&lt;/p&gt;

&lt;p&gt;The solution is to &lt;strong&gt;buffer writes in-memory within the transaction itself&lt;/strong&gt;. Each transaction maintains its own private map of key-value pairs. These buffered writes are invisible to everyone else (every other transaction or read outside transaction). Only at commit time do they get written to the WAL and memtable together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T1: PUT balance 70     (buffered in transaction memory)
T1: GET balance → 70   (read from buffered memory)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The read path should be such that, on every &lt;code&gt;GET&lt;/code&gt; within a transaction we check the buffered writes first. If the key exists there, return the buffered value. Only if the key is not in the buffer do we acquire a read lock and read from the underlying storage.&lt;/p&gt;

&lt;p&gt;Hence, we also add &lt;code&gt;bufferedWriteMap&lt;/code&gt; to the &lt;code&gt;Transaction&lt;/code&gt; struct now,&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;Transaction&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;id&lt;/span&gt;               &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;               &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
    &lt;span class="n"&gt;bufferedWriteMap&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="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;lockAcquiredKeys&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Achieving Atomicity in COMMIT
&lt;/h2&gt;

&lt;p&gt;At commit time, we need to persist all buffered writes. But if we write them to the WAL one at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WAL: [PUT account_A 400]     ← written
     [PUT account_B 600]     ← process crashes here, never written
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We get a partial commit. Account A is debited, account B is not credited. This violates atomicity.&lt;/p&gt;

&lt;p&gt;The solution is to &lt;strong&gt;serialize ALL buffered writes into a single WAL entry.&lt;/strong&gt; The WAL already guarantees that each entry either fully exists on disk or does not (via length prefix + checksum). By packing the entire transaction into one single entry, we can achieve atomicity in a transaction.&lt;/p&gt;

&lt;p&gt;The WAL entry format for a transaction becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[len("TRANSACTION")]["TRANSACTION"][num_writes]
[len(payload_1)][payload_1][len(payload_2)][payload_2]...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a bank transfer committing two writes would produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[11]["TRANSACTION"][2]
[18]["PUT account_A 400"][18]["PUT account_B 600"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This entire sequence is written as a single WAL record with one checksum covering everything. If any byte is missing or corrupt, the entire transaction entry is rejected on recovery.&lt;/p&gt;

&lt;p&gt;After the WAL write, we apply the buffered writes to the memtable and release all locks. Even if the process crashes after the WAL write but before memtable updates, recovery will replay the transaction entry and apply all writes.&lt;/p&gt;

&lt;p&gt;This also means our WAL deserialization logic (which rebuilds the memtable on application startup) needs to be updated. Previously it only understood &lt;code&gt;PUT&lt;/code&gt; commands. Now it must also recognize the &lt;code&gt;TRANSACTION&lt;/code&gt; command and unpack the individual writes from within it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;We discussed several data structures in the problem sections above: &lt;code&gt;keyVsLocksAcquiredMap&lt;/code&gt;, &lt;code&gt;LocksAcquired&lt;/code&gt; for read and write, transaction IDs, &lt;code&gt;lockAcquiredKeys&lt;/code&gt;, and buffered writes as &lt;code&gt;bufferedWriteMap&lt;/code&gt;. Let's put them all together and see how they look as Go structs.&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;TransactionManager&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;Mutex&lt;/span&gt;
    &lt;span class="n"&gt;keyVsLocksAcquiredMap&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;LocksAcquired&lt;/span&gt;
    &lt;span class="n"&gt;nextTxnId&lt;/span&gt;             &lt;span class="kt"&gt;uint64&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;LocksAcquired&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;readerTxnIds&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;writerTxnId&lt;/span&gt;  &lt;span class="kt"&gt;uint64&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;Transaction&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;id&lt;/span&gt;               &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;               &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
    &lt;span class="n"&gt;bufferedWriteMap&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="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;lockAcquiredKeys&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TransactionManager&lt;/code&gt; is shared across all transactions. It owns the lock table and a mutex to protect it. &lt;code&gt;Transaction&lt;/code&gt; is per-session, each &lt;code&gt;BEGIN&lt;/code&gt; creates a new transaction with a fresh ID.&lt;/p&gt;

&lt;p&gt;From the caller's perspective, the API follows the pattern used by golang libraries like GORM: &lt;code&gt;db.Begin()&lt;/code&gt; returns a &lt;code&gt;Transaction&lt;/code&gt; object with a unique ID, and all subsequent operations (&lt;code&gt;Get&lt;/code&gt;, &lt;code&gt;Put&lt;/code&gt;, &lt;code&gt;Commit&lt;/code&gt;) are called on that object.&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="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"balance"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"balance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"70"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Write Path
&lt;/h3&gt;

&lt;p&gt;When a transaction calls &lt;code&gt;PUT&lt;/code&gt;, we acquire the write lock and buffer the value in &lt;code&gt;bufferedWriteMap&lt;/code&gt;. Nothing touches the WAL or memtable.&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tryAcquireWriteLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bufferedWriteMap&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bufferedWriteMap&lt;/span&gt; &lt;span class="o"&gt;=&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bufferedWriteMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tryAcquireWriteLock&lt;/code&gt; function is where the core locking logic resides. &lt;code&gt;keyVsLocksAcquiredMap&lt;/code&gt; is a shared variable that multiple transactions can try to read and update concurrently. Hence the entire function is wrapped in a mutex lock by the caller (&lt;code&gt;Put&lt;/code&gt;), ensuring only one transaction modifies the lock table at a time.&lt;/p&gt;

&lt;p&gt;As the name states, we &lt;em&gt;try&lt;/em&gt; to acquire the lock. It returns an error if some other transaction has already acquired a read or write lock on the key. The only cases where we can safely acquire is when no other transaction holds any lock on the key. If the current transaction already holds a read lock, we upgrade it to a write lock provided that only the current transaction holds a read lock.&lt;/p&gt;

&lt;p&gt;The implementation takes care of the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check if write lock is acquired by some other transaction. If yes, return error.&lt;/li&gt;
&lt;li&gt;If already acquired by the current transaction, return early.&lt;/li&gt;
&lt;li&gt;Check if read locks are held by other transactions. If yes, return error.&lt;/li&gt;
&lt;li&gt;If only the current transaction holds a read lock, upgrade it (clear reader, set writer).&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;writerTxnId&lt;/code&gt; to the current transaction ID and track the key in &lt;code&gt;lockAcquiredKeys&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;tryAcquireWriteLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;locksAcquired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyVsLocksAcquiredMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;readLockAlreadyAcquired&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;locksAcquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;LocksAcquired&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;writerTxnId&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;locksAcquired&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writerTxnId&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;writerTxnId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;writerTxnId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&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;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cannot acquire write lock as write lock acquired by transaction '%d'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;writerTxnId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;writerTxnId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&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;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;readerTxnIds&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;locksAcquired&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readerTxnIds&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readerTxnIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WriteLockNotAcquiredDueToReadLocksError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readerTxnIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;readerTxnIds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&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;readLockAlreadyAcquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
                &lt;span class="n"&gt;locksAcquired&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readerTxnIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WriteLockNotAcquiredDueToReadLocksError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;locksAcquired&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writerTxnId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;readLockAlreadyAcquired&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lockAcquiredKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lockAcquiredKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyVsLocksAcquiredMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;locksAcquired&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;readLockAlreadyAcquired&lt;/code&gt; flag prevents double-tracking the key in &lt;code&gt;lockAcquiredKeys&lt;/code&gt;. If the transaction previously acquired a read lock on this key, the key is already in the list from that earlier call. Adding it again would cause &lt;code&gt;releaseAllLocks&lt;/code&gt; to process it twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read Path
&lt;/h3&gt;

&lt;p&gt;On &lt;code&gt;GET&lt;/code&gt;, we check the buffered writes first (read-your-own-writes) and then fall through to the storage engine.&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tryAcquireReadLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bufferedWriteMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tryAcquireReadLock&lt;/code&gt; implementation follows the same pattern as &lt;code&gt;tryAcquireWriteLock&lt;/code&gt;. &lt;code&gt;keyVsLocksAcquiredMap[key]&lt;/code&gt; is still a shared variable and requires using mutex lock.&lt;/p&gt;

&lt;p&gt;The difference in the logic is that we return an error only if another transaction holds a &lt;strong&gt;write&lt;/strong&gt; lock. Multiple read locks from different transactions can coexist, so existing read locks are not a conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commit Flow
&lt;/h3&gt;

&lt;p&gt;We discussed earlier how atomicity is achieved by serializing all buffered writes into a single WAL entry (see "Achieving Atomicity in COMMIT" above). Here we focus on the order of operations.&lt;/p&gt;

&lt;p&gt;The order matters. This follows the same principle we established in Part 1: &lt;strong&gt;write to WAL first, then update in-memory state.&lt;/strong&gt; If the process crashes after the WAL write but before memtable updates, recovery replays the WAL and rebuilds the memtable. If it crashes before the WAL write, the transaction is simply lost as if it never happened.&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeSingleWalEntryForCommit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&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="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bufferedWriteMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShouldFlush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createSsTableAndClearWalAndMemTable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;releaseAllLocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleanupBufferedWriteMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking through the order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;WAL write first.&lt;/strong&gt; This is the durability guarantee. Once this succeeds, the transaction is committed regardless of what happens next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply to memtable.&lt;/strong&gt; Makes the writes visible to future reads. We write to the memtable directly (&lt;code&gt;txn.db.memTable.Put&lt;/code&gt;) rather than going through &lt;code&gt;db.Put&lt;/code&gt;, since &lt;code&gt;db.Put&lt;/code&gt; would write separate WAL entries for each key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flush check.&lt;/strong&gt; If the memtable is full, flush to SSTable. Same as the normal write path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release locks.&lt;/strong&gt; Other transactions that were blocked can now proceed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup.&lt;/strong&gt; Discard the buffered write map.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;releaseAllLocks&lt;/code&gt; iterates over &lt;code&gt;txn.lockAcquiredKeys&lt;/code&gt; (the list we discussed earlier for efficient release) and for each key, does an O(1) lookup in &lt;code&gt;keyVsLocksAcquiredMap&lt;/code&gt; to clear the lock. If the transaction held a write lock, it sets &lt;code&gt;writerTxnId&lt;/code&gt; to 0. If it held a read lock, it removes the transaction ID from &lt;code&gt;readerTxnIds&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback Flow
&lt;/h3&gt;

&lt;p&gt;Rollback is simpler: release all locks and discard the buffered writes. Since nothing was written to the WAL or the memtable, there is nothing to undo on disk or within in-memory memtable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improvement Areas
&lt;/h2&gt;

&lt;p&gt;Our 2PL implementation works for the basics but has several simplifications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No wait queue.&lt;/strong&gt; When a lock conflict is detected, we return an error immediately. The caller must retry. A production database would maintain a wait queue. Transactions block until the conflicting transaction commits or rolls back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No deadlock detection.&lt;/strong&gt; If T1 holds lock on A and wants B, while T2 holds lock on B and wants A, both will error out. A proper system would detect this cycle and abort one transaction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single global mutex for the lock manager.&lt;/strong&gt; All lock acquisitions and releases go through &lt;code&gt;transactionManager.mu&lt;/code&gt;. Under high concurrency, this becomes a bottleneck. Production databases shard the lock table across multiple mutexes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;2PL vs MVCC.&lt;/strong&gt; Our 2PL approach means readers block writers and writers block readers. PostgreSQL uses &lt;strong&gt;Multi-Version Concurrency Control (MVCC)&lt;/strong&gt; instead where multiple versions of the same row coexist, and each transaction sees a consistent snapshot from its start time. Readers never block writers and vice versa. This is much better for read-heavy workloads but significantly more complex to implement.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;We have a transactional key-value store. Atomic multi-key writes. Isolation from concurrent readers. Rollback on failure.&lt;/p&gt;

&lt;p&gt;In the upcoming parts, we will deep-dive on the SQL layer. This will include: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The intuition and implementation of &lt;code&gt;CREATE TABLE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;SELECT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Implementing secondary indexes for efficient reads.&lt;/li&gt;
&lt;li&gt;Implementing a query planner to choose the most efficient index or read path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code for SaarDB is available here:&lt;br&gt;
GitHub: &lt;a href="https://github.com/gagandeepahuja09/saardb" rel="noopener noreferrer"&gt;https://github.com/gagandeepahuja09/saardb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
    </item>
    <item>
      <title>Building SaarDB, Part 3: Compaction</title>
      <dc:creator>Gagandeep Singh Ahuja</dc:creator>
      <pubDate>Mon, 20 Jul 2026 04:25:08 +0000</pubDate>
      <link>https://dev.to/gagandeepahuja09/building-saardb-part-3-compaction-2fhc</link>
      <guid>https://dev.to/gagandeepahuja09/building-saardb-part-3-compaction-2fhc</guid>
      <description>&lt;p&gt;In Parts 1 and 2, we built a storage engine: WAL for durability and LSM tree for efficient disk reads.&lt;/p&gt;

&lt;p&gt;This works. But there is a problem growing quietly in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem: SSTable Files Keep Growing
&lt;/h2&gt;

&lt;p&gt;Every memtable flush creates a new SSTable file. After 10 flushes, you have 10 files. After 1000 flushes, you have 1000 files.&lt;/p&gt;

&lt;p&gt;Now consider what happens when you serve a GET request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET user_42

Search SSTable 1000 (newest) → not found
Search SSTable 999           → not found
Search SSTable 998           → not found
...
Search SSTable 1             → found!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the worst case, we search ALL 1000 files before finding the key (or confirming it doesn't exist). Each search binary searching the index block, and potentially reading a data block from disk.&lt;/p&gt;

&lt;p&gt;This is called &lt;strong&gt;read amplification&lt;/strong&gt;: the ratio of total data read to the actual data requested. We wanted 1 key-value pair. We had to touch 1000 files to find it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As the number of SSTable files grows, read performance degrades linearly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Problem: Duplicate Keys Waste Space
&lt;/h2&gt;

&lt;p&gt;There is a second problem. Consider this write pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice     → flushed to SSTable 1
PUT user_1 Bob       → flushed to SSTable 2
PUT user_1 Charlie   → flushed to SSTable 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three SSTable files each contain a value for &lt;code&gt;user_1&lt;/code&gt;. But only the newest value (&lt;code&gt;Charlie&lt;/code&gt; in SSTable 3) matters. The entries in SSTables 1 and 2 are pure waste. They consume disk space but will never be returned to a reader.&lt;/p&gt;

&lt;p&gt;This is called &lt;strong&gt;space amplification&lt;/strong&gt;: the ratio of total storage used to the actual live data size. If a key is updated 100 times, we store 100 copies. Space amplification = 100x for that key.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Append-only writes are fast, but they accumulate garbage over time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Solution: Merge Files Periodically
&lt;/h2&gt;

&lt;p&gt;The idea is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read multiple SSTable files&lt;/li&gt;
&lt;li&gt;For each key, keep only the newest value&lt;/li&gt;
&lt;li&gt;Write a single compacted file with the deduplicated data&lt;/li&gt;
&lt;li&gt;Delete the old files
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before compaction:
SSTable 1: [user_1=Alice, user_2=Bob]
SSTable 2: [user_1=Charlie, user_3=Dave]
SSTable 3: [user_2=Eve, user_4=Frank]

After compaction:
SSTable 5: [user_1=Charlie, user_2=Eve, user_3=Dave, user_4=Frank]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fewer files means faster reads (less read amplification). Deduplicated keys means less wasted space (less space amplification).&lt;/p&gt;

&lt;p&gt;But compaction introduces a new cost: &lt;strong&gt;write amplification&lt;/strong&gt;. We are reading and rewriting data that has not changed. &lt;code&gt;user_3=Dave&lt;/code&gt; and &lt;code&gt;user_4=Frank&lt;/code&gt; were perfectly fine in their original files. We read them, wrote them again, and deleted the originals. This is extra I/O for data that was already correct.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Compaction trades write amplification for reduced read and space amplification.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This creates a three-way tension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read Amplification ←→ Write Amplification ←→ Space Amplification
       ↑                       ↑                      ↑
  (more files =             (compaction             (duplicates
   slower reads)             rewrites data)          waste space)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot minimize all three simultaneously. Every compaction strategy is a different point in this tradeoff space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem: When Do We Trigger Compaction?
&lt;/h2&gt;

&lt;p&gt;If we compact after every single flush, we minimize read and space amplification but maximize write amplification because we are constantly rewriting data.&lt;/p&gt;

&lt;p&gt;If we never compact, write amplification is zero but read and space amplification grow forever.&lt;/p&gt;

&lt;p&gt;We need a middle ground. Think about what changes as you increase the threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Threshold = 2:&lt;/strong&gt; Almost every flush triggers compaction. You rewrite data constantly. Write amplification is very high for marginal read benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold = 4:&lt;/strong&gt; You batch a few files before merging. Compaction happens less frequently, and you amortize the I/O cost across more files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold = 8 or 10:&lt;/strong&gt; Compaction is rare, so write amplification is low. But reads degrade because you search through 8-10 files before compaction kicks in. And when compaction finally runs, it is a bigger burst of I/O.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our implementation uses 4 as a simple learning threshold. Production systems use more sophisticated compaction policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compaction Runs in the Background
&lt;/h2&gt;

&lt;p&gt;Up until now, we have been discussing compaction at a conceptual level: what it does, why it helps, when to trigger it. The implementation is where things get interesting. Since compaction runs concurrently with reads and writes, we need to be careful about shared state.&lt;/p&gt;

&lt;p&gt;Compaction is expensive. It reads all data blocks from multiple files, merges them, and writes a new file. We cannot block all reads and writes while this happens, so it runs in a background goroutine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is shared state?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Shared state means that multiple goroutines can read and write the same variables concurrently. Without a mutex lock, this leads to data races and unexpected behaviour.&lt;br&gt;
Within compaction, the list of files, the index blocks and index offsets are all shared variables.&lt;/p&gt;

&lt;p&gt;All of these follow the same access pattern: they are read by the &lt;code&gt;Get&lt;/code&gt; function and written by &lt;code&gt;flush&lt;/code&gt; during writes. We saw this in our previous blog. Now compaction adds another writer running in parallel, deleting old files and replacing them with new ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side Note&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;go test -race ./...&lt;/code&gt; is a great way to identify shared state if we are not able to spot it ourselves.&lt;/p&gt;
&lt;h3&gt;
  
  
  Read and Write Locks
&lt;/h3&gt;

&lt;p&gt;Before deep-diving into race condition and potential problems, we should be aware of the two ways of acquiring mutex locks: read (shared) locks and write (exclusive) locks. &lt;/p&gt;

&lt;p&gt;Multiple processes can hold read locks on the same key simultaneously. But a write lock is exclusive, meaning no other process can hold any lock on that key while a write lock is active.&lt;/p&gt;

&lt;p&gt;This is because, two readers cannot corrupt each other since neither is modifying data. But a writer must be alone because any concurrent reader would see a partially updated or inconsistent value.&lt;/p&gt;

&lt;p&gt;Hence, from a performance perspective it is critical that we use write lock only when a write operation is being used.&lt;/p&gt;

&lt;p&gt;Golang provides RLock and Lock in &lt;code&gt;sync.Mutex&lt;/code&gt; package for read lock and write lock respectively. You will see more of this being used especially in the next blog where we will require acquiring locks within database rows.&lt;/p&gt;
&lt;h3&gt;
  
  
  Race Conditions Due To Shared State
&lt;/h3&gt;

&lt;p&gt;Before diving into implementation, we need to better understand the shared state and potential race conditions that can come up.&lt;/p&gt;
&lt;h4&gt;
  
  
  Problem 1: New files can arrive mid-compaction
&lt;/h4&gt;

&lt;p&gt;Let's say we start compacting files [1, 2, 3, 4]. While the merge is running, a new write triggers a memtable flush, creating file 5. File 5 was not part of our compaction. Compaction also created a file 6 as its output. If compaction blindly replaces the entire file list with its output, file 5 disappears and there is only a single file 6 with lost data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution: Snapshot the file list before compaction starts&lt;/strong&gt;&lt;br&gt;
We snapshot the file list at the start. Snapshot here just means that we store the list of files being compacted in an in-memory variable. We also acquire a read lock while doing so.&lt;/p&gt;

&lt;p&gt;Later, when we swap out old files and add the newly compacted file, we know exactly which files were part of compaction and should be deleted. Files that were not part of the snapshot (like file 5) are left untouched.&lt;/p&gt;

&lt;p&gt;The entire swap operation should also be behind a write lock, since regular flushes could be happening at the same time.&lt;/p&gt;
&lt;h4&gt;
  
  
  Problem 2: Order of the files
&lt;/h4&gt;

&lt;p&gt;In the last blog, we ensured that file names are monotonically increasing. This was important to identify the newest file. But in the above example, compaction's output file gets ID 6 (next auto-incremented), while the flush that happened during compaction created file 5. File 5 has the newest data but a lower numeric ID than file 6. File names are no longer an indicator of how new the data is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;br&gt;
We maintain a &lt;code&gt;manifest.json&lt;/code&gt; file (and an in-memory reference) that tracks the expected search order. In the above case, the order should be [5, 6]: file 5 (newest data from the flush) is searched first, file 6 (compacted older data) is searched after.&lt;/p&gt;

&lt;p&gt;Our swap function should update the file list in this order and persist it to &lt;code&gt;manifest.json&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Problem 3: Two compactions can start at once
&lt;/h4&gt;

&lt;p&gt;While compaction is running, a series of flushes happening in parallel can push the file count above the threshold again. Now two compactions are merging overlapping file sets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;br&gt;
We need a guard to ensure only one compaction runs at a time. We maintain a flag called &lt;code&gt;compacting&lt;/code&gt;. A new compaction can only start when this flag is false.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;compacting&lt;/code&gt; is itself a shared variable and should be read or written via a mutex lock. We set it to true when compaction starts and back to false when it ends. Our &lt;code&gt;shouldRunCompaction&lt;/code&gt; function checks this flag in addition to checking the file count.&lt;/p&gt;
&lt;h4&gt;
  
  
  Problem 4: Readers might be using files we want to delete.
&lt;/h4&gt;

&lt;p&gt;Let's say compaction finishes and wants to delete files 1 to 4. But a GET request is currently reading file 1's data block. If we delete the file now, that reader gets a corrupted read or a crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;br&gt;
We need to make sure all in-flight reads are done before we swap out the old files. Our &lt;code&gt;swap&lt;/code&gt; function takes a write lock before replacing the file list. Since readers hold a read lock for the duration of their &lt;code&gt;Get&lt;/code&gt; call, the write lock blocks until every in-flight reader releases their lock. Only then does the swap proceed.&lt;/p&gt;

&lt;p&gt;All four problems are solved with Go's &lt;code&gt;sync.RWMutex&lt;/code&gt; and a &lt;code&gt;compacting&lt;/code&gt; flag. Let's look at the implementation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  When to trigger compaction
&lt;/h3&gt;

&lt;p&gt;The compaction process should only start when memtable is being flushed to SSTable. We also require two additional checks: enough files to justify the cost (4 in this case), and no compaction already in progress.&lt;/p&gt;

&lt;p&gt;Note that the &lt;code&gt;go&lt;/code&gt; keyword in &lt;code&gt;go db.ssTable.RunCompaction()&lt;/code&gt; in Golang is used to trigger a goroutine (a light-weight thread) in parallel in Golang.&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;flushMemtableToSsTable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ssTableFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Iterate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShouldRunCompaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunCompaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ShouldRunCompaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compacting&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compaction Logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RunCompaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Set compacting flag&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compacting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compacting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Snapshot files to compact&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;filesToCompact&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filesToCompact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Build compacted map&lt;/span&gt;
    &lt;span class="n"&gt;compactedMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildCompactedMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filesToCompact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 4. Sort keys for ordered SSTable output&lt;/span&gt;
    &lt;span class="n"&gt;sortedKeys&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sortedKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compactedMap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 5. Create iterator and write compacted file&lt;/span&gt;
    &lt;span class="n"&gt;iterator&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;sortedKeys&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compactedMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;compactedFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;compactedIndexOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compactedIndexBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeToFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compactedFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 6. Atomic swap of file and index arrays&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomicSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compactedFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filesToCompact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compactedIndexBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compactedIndexOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 7. Delete old files&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;filesToCompact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 1 sets the &lt;code&gt;compacting&lt;/code&gt; flag so that &lt;code&gt;ShouldRunCompaction&lt;/code&gt; returns false for the duration. This prevents double compaction (problem 3).&lt;/p&gt;

&lt;p&gt;Step 2 snapshots the file list under a read lock. This is the fix for the "new file arrives mid-compaction" problem (problem 1). Any files flushed after this point are not part of &lt;code&gt;filesToCompact&lt;/code&gt; and will be preserved during the swap in step 6.&lt;/p&gt;

&lt;p&gt;Steps 3-5 are the actual merge. &lt;code&gt;buildCompactedMap&lt;/code&gt; reads all data blocks from the snapshotted files, oldest to newest. Since it iterates oldest-first, newer values for the same key naturally overwrite older ones in the map:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;buildCompactedMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compactedMap&lt;/span&gt; &lt;span class="o"&gt;:=&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;indexOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getIndexOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="n"&gt;indexOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;extractValueFromSsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;extractValueFromSsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;compactedMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;  &lt;span class="c"&gt;// newer file's value overwrites older&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;compactedMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 6 is the most critical part. This is where we solve the concurrent reader problem (problem 4):&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;atomicSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compactedFile&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oldFiles&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;compactedIndexBlock&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;indexBlockEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compactedIndexOffset&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;oldFilesMap&lt;/span&gt; &lt;span class="o"&gt;:=&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;oldFiles&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;oldFilesMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Compacted file first (oldest data), then new files created during compaction&lt;/span&gt;
    &lt;span class="n"&gt;swappedFiles&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;compactedFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;swappedIndexBlocks&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[][]&lt;/span&gt;&lt;span class="n"&gt;indexBlockEntry&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;compactedIndexBlock&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;swappedIndexOffsets&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;compactedIndexOffset&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;oldFilesMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;swappedFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swappedFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;swappedIndexBlocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swappedIndexBlocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexBlocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;swappedIndexOffsets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swappedIndexOffsets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexOffsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swappedFiles&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexBlocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swappedIndexBlocks&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileNames&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;saveManifest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write lock ensures no reader is mid-read during the swap. Readers hold &lt;code&gt;RLock&lt;/code&gt; during their entire read operation, so acquiring &lt;code&gt;Lock&lt;/code&gt; here blocks until all in-flight reads complete. After the swap, new readers see the compacted file. Old readers have already finished.&lt;/p&gt;

&lt;p&gt;The compacted file is placed first in the list because it contains the oldest data. Any files created during compaction (not in &lt;code&gt;oldFilesMap&lt;/code&gt;) are appended after it. This preserves the newest-to-oldest search order that &lt;code&gt;Get()&lt;/code&gt; relies on (solves problem 2).&lt;/p&gt;

&lt;p&gt;After swap has successfully happened in step 6, we don't need the old files which got compacted and hence it is safe to delete them in step 7.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark Results
&lt;/h2&gt;

&lt;p&gt;We benchmarked read performance with and without compaction. The setup writes repeated keys to generate duplicate entries, then the benchmark measures bulk &lt;code&gt;Get&lt;/code&gt; operations (&lt;a href="https://github.com/gagandeepahuja09/golang-db/blob/d9caafb/perf_test.go" rel="noopener noreferrer"&gt;without compaction&lt;/a&gt;, &lt;a href="https://github.com/gagandeepahuja09/golang-db/blob/8b0857e/perf_test.go" rel="noopener noreferrer"&gt;with compaction&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;With Compaction&lt;/th&gt;
&lt;th&gt;Without Compaction&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Iterations&lt;/td&gt;
&lt;td&gt;3,104&lt;/td&gt;
&lt;td&gt;656&lt;/td&gt;
&lt;td&gt;~4.7x more throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time/op&lt;/td&gt;
&lt;td&gt;386,022 ns&lt;/td&gt;
&lt;td&gt;1,573,801 ns&lt;/td&gt;
&lt;td&gt;~4.1x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory/op&lt;/td&gt;
&lt;td&gt;203,957 B&lt;/td&gt;
&lt;td&gt;848,196 B&lt;/td&gt;
&lt;td&gt;~4.2x less memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocs/op&lt;/td&gt;
&lt;td&gt;3,459&lt;/td&gt;
&lt;td&gt;13,429&lt;/td&gt;
&lt;td&gt;~3.9x fewer allocations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The improvement is across the board. Without compaction, each read potentially searches through many more files. The extra file I/O shows up in time, memory (more buffers), and allocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known Limitations
&lt;/h2&gt;

&lt;p&gt;Our compaction implementation is simple and has several limitations worth acknowledging:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;buildCompactedMap&lt;/code&gt; loads everything into memory.&lt;/strong&gt; Production databases use streaming merge iterators that read entries from multiple files in sorted order without loading everything at once. Our approach works for small datasets but would blow up RAM with large ones.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No tombstones.&lt;/strong&gt; We have no way to represent deletes. If a key is deleted, its old value still lives in SSTables until it happens to be overwritten. A proper implementation would write a tombstone marker that compaction can use to discard the key entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No leveled compaction.&lt;/strong&gt; We merge all L0 files into one. Production LSM trees like LevelDB and RocksDB use tiered or leveled compaction with multiple levels (L0, L1, L2...), where each level is progressively larger. This gives much better control over the amplification tradeoffs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;We now have a complete storage engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WAL&lt;/strong&gt; for durability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memtable&lt;/strong&gt; for fast in-memory writes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSTables&lt;/strong&gt; for efficient disk reads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compaction&lt;/strong&gt; to keep file count and duplicate keys manageable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the basics of a persisted key-value store. But for building a relational database, we need ACID guarantees and we have only focused on durability till now.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens when you need multiple writes to succeed or fail together? (Atomicity) What happens when two users read and write the same key concurrently? (Isolation)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Part 4, we will build &lt;strong&gt;transactions&lt;/strong&gt; with Two-Phase Locking: atomic multi-key writes, isolation from concurrent readers, and rollback on failure.&lt;/p&gt;

&lt;p&gt;The code for SaarDB is available here:&lt;br&gt;
GitHub: &lt;a href="https://github.com/gagandeepahuja09/saardb" rel="noopener noreferrer"&gt;https://github.com/gagandeepahuja09/saardb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building SaarDB, Part 2: LSM Trees</title>
      <dc:creator>Gagandeep Singh Ahuja</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:16:25 +0000</pubDate>
      <link>https://dev.to/gagandeepahuja09/building-saardb-part-2-lsm-trees-3615</link>
      <guid>https://dev.to/gagandeepahuja09/building-saardb-part-2-lsm-trees-3615</guid>
      <description>&lt;p&gt;In Part 1, SaarDB had two pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Write Ahead Log (WAL) for durability,&lt;/li&gt;
&lt;li&gt;an in-memory map for serving reads and writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works as a starting point, but it has limits. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The in-memory map cannot grow forever, we are bounded by RAM limitations.
&lt;/li&gt;
&lt;li&gt;Replaying a larger WAL on every startup gets slower.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the next problem is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we keep writes simple, but move older data into a searchable disk format?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why WAL Alone Is Not Enough
&lt;/h2&gt;

&lt;p&gt;WAL is fundamentally an append-only log. This is excellent for durability and sequential disk writes, but eventually it creates two major problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: Repeated Keys
&lt;/h3&gt;

&lt;p&gt;Imagine the following operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice
PUT user_2 Bob
PUT user_1 Charlie
PUT user_1 David
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WAL now contains multiple values for the same key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user_1 -&amp;gt; Alice]
[user_2 -&amp;gt; Bob]
[user_1 -&amp;gt; Charlie]
[user_1 -&amp;gt; David]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But during reads, only the latest value matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user_1 -&amp;gt; David
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As updates increase, the WAL keeps growing while also accumulating stale values. Even if we periodically delete stale values, another much bigger problem still remains.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: Startup Gets Slow, and Memory Runs Out
&lt;/h3&gt;

&lt;p&gt;Suppose the database has accepted millions of writes.&lt;/p&gt;

&lt;p&gt;The WAL keeps growing on disk. The in-memory map also keeps growing because every live key-value pair is kept in RAM.&lt;/p&gt;

&lt;p&gt;During normal reads, this is fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET user_987654
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database can simply check the in-memory map.&lt;/p&gt;

&lt;p&gt;But now imagine the process restarts.&lt;/p&gt;

&lt;p&gt;To rebuild that in-memory map, the database has to read the WAL from the beginning and replay every write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice
PUT user_2 Bob
PUT user_1 Charlie
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a small WAL, this is fine.&lt;/p&gt;

&lt;p&gt;For a huge WAL, startup becomes slow because the database must process a large file before it can serve reads. And after replay finishes, all live keys still need to fit in memory.&lt;/p&gt;

&lt;p&gt;Could we avoid rebuilding the full in-memory map?&lt;/p&gt;

&lt;p&gt;We can do that by reading directly from the WAL during &lt;code&gt;GET&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But that is slow. The WAL is not sorted or indexed by key. It is just a long list of writes. So finding one key means scanning WAL entries one by one, on disk. And because older entries may be followed by newer updates for the same key, the database may need to keep scanning to know the latest value.&lt;/p&gt;

&lt;p&gt;That turns every read into a linear search over a growing file.&lt;/p&gt;

&lt;p&gt;So WAL alone gives us two bad choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Replay the entire WAL on startup and keep everything in memory.&lt;/li&gt;
&lt;li&gt;Avoid replaying everything, but make reads slow because they must search through the WAL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither works well once the data grows.&lt;/p&gt;

&lt;p&gt;A WAL is great for remembering writes in order. It is not a good long-term format for searching keys.&lt;/p&gt;

&lt;p&gt;So we need a second structure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep recent writes in memory, but move older data to disk in a format that can be searched efficiently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That disk format should help us quickly narrow down where a key may exist. The most common way to reduce search space efficiently is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Binary Search&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But this leads to another interesting problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Challenge: How Do You Apply Binary Search On Disk?
&lt;/h2&gt;

&lt;p&gt;Binary search is trivial in memory. Arrays support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;direct indexing,&lt;/li&gt;
&lt;li&gt;constant-time jumps,&lt;/li&gt;
&lt;li&gt;cheap random access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disk files do not naturally behave like arrays. The fundamental reason is that entries are variable-length. In an array, every element is the same size, so computing the position of the Nth element is trivial. But in a disk file storing key-value pairs, each entry can be a different size. Without reading and parsing every entry before it, there is no way to know where the Nth entry begins.&lt;/p&gt;

&lt;p&gt;So the real challenge becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we structure data on disk such that binary search becomes possible?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LSM (Log-Structured Merge) tree storage solves this using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSTables (on disk)&lt;/li&gt;
&lt;li&gt;Memtables (in memory)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SSTable: Making Disk Reads Efficient
&lt;/h2&gt;

&lt;p&gt;SSTable stands for &lt;code&gt;Sorted String Table&lt;/code&gt;.&lt;br&gt;
The key idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Store keys on disk in sorted order to allow efficient search.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But applying binary search directly on one huge disk file is still not straightforward. So SSTables organize data carefully.&lt;/p&gt;
&lt;h3&gt;
  
  
  How SSTables Are Structured
&lt;/h3&gt;

&lt;p&gt;Instead of storing one massive blob of sorted data, SSTables divide data into smaller blocks. Typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each block is bounded by size (for example ~100 KB),&lt;/li&gt;
&lt;li&gt;keys inside each block remain sorted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data Block 1:
[user_1]
[user_2]
[user_3]

Data Block 2:
[user_4]
[user_5]
[user_6]

Data Block 3:
[user_7]
[user_8]
[user_9]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice something important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All keys in Block 2 &amp;gt; all keys in Block 1
All keys in Block 3 &amp;gt; all keys in Block 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ordering enables efficient lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Index Block: The Real Enabler
&lt;/h3&gt;

&lt;p&gt;Each SSTable also contains an Index Block. The index block stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the starting key of every data block,&lt;/li&gt;
&lt;li&gt;the offset of that block inside the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user_1 -&amp;gt; offset 0
user_4 -&amp;gt; offset 1024
user_7 -&amp;gt; offset 2048
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now suppose we want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET user_5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We first load the index block into memory.&lt;/p&gt;

&lt;p&gt;Then we binary search the block-start keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user_1, user_4, user_7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The largest key less than or equal to &lt;code&gt;"user_5"&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user_4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we immediately know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If &lt;code&gt;"user_5"&lt;/code&gt; exists, it must exist in the block starting with &lt;code&gt;"user_4"&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of searching the entire SSTable, we reduced the search space to a single block. This is the core idea behind SSTables.&lt;/p&gt;

&lt;p&gt;Once the correct block is found, entries within the block are scanned sequentially till we either find the required key (key found case) or till the entire block is completely read (key not found case).&lt;/p&gt;

&lt;p&gt;This leads us to the core lookup rule used in SSTables:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Find the largest block-start key less than or equal to the input key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  SSTable File Layout
&lt;/h3&gt;

&lt;p&gt;A typical SSTable layout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------+
| Data Block 1|
+-------------+
| Data Block 2|
+-------------+
| Data Block 3|
+-------------+
| Index Block |
+-------------+
| Footer      |
+-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Do We Need A Footer?
&lt;/h3&gt;

&lt;p&gt;Interesting problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we know where the index block starts?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The index block is usually near the end of the file. But during reads, we don't want to scan the entire file just to find it.&lt;/p&gt;

&lt;p&gt;So SSTables store a small Footer Block at the end. Index block can of variable bytes, hence there is no way to know the position of index block until we store the index block offset. On the other hand, if we store the index block offset at the start (header) or end (footer) of the file, we can easily find the index block offset. This is because header and footer are fixed positions in the file.&lt;/p&gt;

&lt;p&gt;The footer typically contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;index block offset,&lt;/li&gt;
&lt;li&gt;metadata about the SSTable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows readers to directly jump to the index block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memtable: How Data Enters the SSTable
&lt;/h2&gt;

&lt;p&gt;SSTables require their keys to be sorted. So how does data get into an SSTable in sorted order?&lt;/p&gt;

&lt;p&gt;This is where the Memtable comes in. A Memtable is simply an in-memory sorted map.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-memory&lt;/strong&gt; means we are bounded by RAM. We cannot keep writing to memory indefinitely. At some point, data must be written to disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sorted&lt;/strong&gt; is what makes the flush to disk efficient. Because the Memtable maintains keys in order, when it is time to write an SSTable, the data is already sorted. There is no need for an expensive sort step. The Memtable simply iterates its keys in order and writes them out sequentially.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The primary purpose of keeping the Memtable sorted is not to speed up reads. It is to make the flush to SSTable efficient.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Whenever writes arrive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They are appended to WAL for durability.&lt;/li&gt;
&lt;li&gt;They are inserted into Memtable for fast reads/writes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because Memtable is sorted, keys are always maintained in order. Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user_1  -&amp;gt; Alice
user_2  -&amp;gt; Bob
user_3  -&amp;gt; Charlie
user_4  -&amp;gt; David
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But we cannot keep growing Memtable forever because RAM is limited. So eventually:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Memtable is flushed to disk as an SSTable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Write Path
&lt;/h2&gt;

&lt;p&gt;Now let's connect the intuition with implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge: How Do We Flush a Memtable to an SSTable?
&lt;/h3&gt;

&lt;p&gt;When Memtable reaches a size threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new SSTable file is created,&lt;/li&gt;
&lt;li&gt;Memtable contents are written in sorted order,&lt;/li&gt;
&lt;li&gt;Memtable is reset.&lt;/li&gt;
&lt;li&gt;WAL can be cleared because those key-values no longer need to be replayed from the log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since Memtable is already sorted, SSTable creation becomes efficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing Data Blocks
&lt;/h3&gt;

&lt;p&gt;While flushing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Memtable keys are iterated in sorted order.&lt;/li&gt;
&lt;li&gt;Key-value pairs are written sequentially into data blocks.&lt;/li&gt;
&lt;li&gt;Once the block size threshold is reached, a new block is started.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is different from the Memtable threshold. The Memtable threshold decides when a flush starts. The block threshold only decides how the new SSTable file is split into smaller data blocks.&lt;/p&gt;

&lt;p&gt;During the part 1, we saw the following encoding format while writing to disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[length_of_key][key][length_of_value][value]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[5][apple][2][10]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Length prefixes are important because:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;keys and values are variable-sized&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Indicating length of the key before the start of the key enables readers to know how many bytes to read.&lt;/p&gt;

&lt;p&gt;While writing data blocks, we also maintain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;block starting key,&lt;/li&gt;
&lt;li&gt;block offset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are later used to build the index block.&lt;/p&gt;

&lt;p&gt;Here is the core loop from our implementation (&lt;code&gt;sstable/sstable.go&lt;/code&gt;) that writes data blocks:&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="n"&gt;iteratorFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;blockFirstKey&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;blockFirstKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// [length_of_key][key][length_of_value][value]&lt;/span&gt;
    &lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt; &lt;span class="o"&gt;:=&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;span class="n"&gt;ssTableEntryBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;blockLength&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ssTableBlockBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableBlockBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssTableEntryBuf&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;blockLength&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blockLength&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// one data block completed&lt;/span&gt;
        &lt;span class="n"&gt;indexBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexBlockEntry&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;blockFirstKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blockStartOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableBlockBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c"&gt;// start new block&lt;/span&gt;
        &lt;span class="n"&gt;blockStartOffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;
        &lt;span class="n"&gt;blockFirstKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
        &lt;span class="n"&gt;blockLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;ssTableBlockBuf&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how this single loop handles the work inside a Memtable flush: length-prefix encoding each entry, accumulating entries into a block buffer, writing the current data block to the SSTable file when the block size threshold is exceeded, and tracking the first key and offset for the index block.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing Strategy: Preserving Sequential Writes
&lt;/h3&gt;

&lt;p&gt;One of the biggest reasons WAL performs well is that it relies on append-only sequential disk writes. SSTable creation preserves this exact property. During a Memtable flush, we create a new file and write data blocks, then the index block, then the footer. All are written sequentially, never going back to modify earlier parts of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    [Data Blocks][Index Block][Footer Block]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So even though SSTables solve the problem of efficient reads, they still preserve the append-only write pattern that made WAL fast in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing Index Block And Footer Block
&lt;/h3&gt;

&lt;p&gt;Using the same length-prefix encoding from data blocks, each index block entry stores:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the starting key of a data block,&lt;/li&gt;
&lt;li&gt;the offset of that block within the SSTable file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This leads to the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[length_of_start_key][start_key][offset]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice an important distinction here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;start_key&lt;/code&gt; is variable-sized, so we prefix it with its length.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;offset&lt;/code&gt; is a fixed-size unsigned 32-bit integer.&lt;/p&gt;

&lt;p&gt;Since the size of the offset is already known during reads, we do not need to prefix it with a length.&lt;/p&gt;

&lt;p&gt;This leads to an important encoding rule commonly used in storage systems:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Variable-sized data types usually require length prefixes.&lt;/p&gt;

&lt;p&gt;Fixed-sized data types usually do not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The footer block follows the same idea.&lt;/p&gt;

&lt;p&gt;Since the footer only stores the index block offset, its structure simply becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[index_offset]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During reads, this footer allows us to directly jump to the index block without scanning the entire SSTable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read Path
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem: A Key Can Exist In Multiple Places
&lt;/h3&gt;

&lt;p&gt;An interesting property of LSM Trees is that the same key can exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in Memtable,&lt;/li&gt;
&lt;li&gt;in multiple SSTables,&lt;/li&gt;
&lt;li&gt;and potentially with different values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This happens because updates do not immediately overwrite older values on disk.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice
PUT user_1 Charlie
PUT user_1 David
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older SSTables may still contain stale values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSTable 1 -&amp;gt; user_1 = Alice
SSTable 2 -&amp;gt; user_1 = Charlie
Memtable  -&amp;gt; user_1 = David
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During reads, only the latest value matters.&lt;/p&gt;

&lt;p&gt;So while serving a &lt;code&gt;GET key&lt;/code&gt; query, the database must search in an order that guarantees the newest value is found first.&lt;/p&gt;

&lt;p&gt;This leads to the following mental model for reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memtable -&amp;gt; Newest SSTable -&amp;gt; Older SSTables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as the key is found, the search stops.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge: How Do We Search Efficiently Inside An SSTable?
&lt;/h3&gt;

&lt;p&gt;Naively reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;footer,&lt;/li&gt;
&lt;li&gt;then index block,&lt;/li&gt;
&lt;li&gt;during every query
would still create repeated disk reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So databases usually cache:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;index blocks,&lt;/li&gt;
&lt;li&gt;metadata,&lt;/li&gt;
&lt;li&gt;file handles
in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes reads significantly faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Searching Inside A File
&lt;/h3&gt;

&lt;p&gt;Once the index block is in memory, searching an SSTable for a key involves four steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Binary search the index block.&lt;/strong&gt; We use the same "largest key less than or equal to the input" rule from earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Calculate the exact byte range to read.&lt;/strong&gt; Once we know which data block to search, we need to figure out: &lt;br&gt;
    * where it starts and &lt;br&gt;
    * ends on disk. &lt;br&gt;
The start offset comes directly from the matched index entry. The end offset comes from the &lt;em&gt;next&lt;/em&gt; index entry's offset. Or if this is the last data block, the end offset is the start of the index block itself (which we already stored in memory during startup).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Read only that one block from disk.&lt;/strong&gt; This is the key insight. We use &lt;code&gt;file.ReadAt()&lt;/code&gt; golang function to read exactly the bytes between the start and end offsets. We are not reading the entire file. We are not even reading from the beginning of the file. We jump directly to the relevant block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Sequential scan within the block.&lt;/strong&gt; Once the block is in memory, we decode entries one by one using the length-prefix format. Which is to: &lt;br&gt;
    * read the key length, &lt;br&gt;
    * read that many bytes for the key, &lt;br&gt;
    * read the value length, &lt;br&gt;
    * read that many bytes for the value&lt;br&gt;
    * and then check if the key matches.&lt;/p&gt;

&lt;p&gt;Here is the &lt;code&gt;Get()&lt;/code&gt; function from our implementation (&lt;code&gt;sstable/sstable.go&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// newest file to oldest file&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLevelFiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;ssTableIndex&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexBlocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getLowerBound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;endOffset&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexOffsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;endOffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueFromSsTableDataBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lowerBoundSliceIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reverse iteration&lt;/strong&gt; (&lt;code&gt;len - 1&lt;/code&gt; down to &lt;code&gt;0&lt;/code&gt;): Newest files are searched first so that the most recent value for a key is found first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getLowerBound&lt;/code&gt; returning &lt;code&gt;-1&lt;/code&gt;&lt;/strong&gt;: The key is smaller than every block-start key in this file. No point reading any block, so we &lt;code&gt;continue&lt;/code&gt; to the next file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;endOffset&lt;/code&gt; calculation&lt;/strong&gt;: If the matched block is the last data block, the end offset defaults to &lt;code&gt;indexOffsets[i]&lt;/code&gt;: the start of the index block. Otherwise, it uses the next index entry's offset. This gives us the precise byte range for the block we need to read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;continue&lt;/code&gt; on empty value with no error&lt;/strong&gt;: The key wasn't found in this file's candidate block, so we try the next (older) file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we exhaust all files without finding the key, &lt;code&gt;Get()&lt;/code&gt; returns an empty string with no error. Which just means that the key simply doesn't exist in the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup: Preparing SSTables for Reads
&lt;/h2&gt;

&lt;p&gt;Earlier, we saw that SSTable reads depend on two things being ready in memory for efficient reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open file handles,&lt;/li&gt;
&lt;li&gt;index blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We need to prepare both of them during database startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Open existing SSTable files.&lt;/strong&gt; These files were created by earlier Memtable flushes. The database opens them in the order it needs for reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Keep file handles open.&lt;/strong&gt; Instead of opening and closing files on every &lt;code&gt;Get()&lt;/code&gt;, the database keeps handles ready for reuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Load index blocks into memory.&lt;/strong&gt; For each SSTable, it reads the footer, which is exactly the last 4 bytes of the file. That footer tells us where the index block starts. Then the database reads that index block and parses it into memory.&lt;/p&gt;

&lt;p&gt;This is the function that loads the index block for one SSTable file:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SsTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;buildIndexFromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;indexBlockEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;indexOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getIndexOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;indexBlockLength&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;indexBlockBuf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="n"&gt;indexBlockLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexBlockBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexOffset&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;ssTableIndex&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;indexBlockEntry&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexBlockLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;keyLengthBuf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;indexBlockBuf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;keyLength&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyLengthBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&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;indexBlockBuf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyLength&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;offsetBuf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;indexBlockBuf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offsetBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;ssTableIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexBlockEntry&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexOffset&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ssTableIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Footer read&lt;/strong&gt;: &lt;code&gt;getIndexOffset&lt;/code&gt; reads the last 4 bytes to find where the index block starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precise byte range&lt;/strong&gt;: reads exactly the bytes between &lt;code&gt;indexOffset&lt;/code&gt; and &lt;code&gt;fileSize - 4&lt;/code&gt;: no more, no less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same length-prefix decoding&lt;/strong&gt;: the loop mirrors the data block decoding pattern: 4-byte key length, key bytes, 4-byte offset and builds the in-memory index entry by entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this, the earlier &lt;code&gt;Get()&lt;/code&gt; implementation has everything it expects: file handles it can reuse and index blocks it can search without first scanning the SSTable file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replaying WAL into the Memtable
&lt;/h3&gt;

&lt;p&gt;SSTables cover data that has already been flushed. But writes after the last flush still live only in the WAL.&lt;/p&gt;

&lt;p&gt;This is the same recovery idea from Part 1. On startup, SaarDB replays WAL entries from oldest to newest.&lt;/p&gt;

&lt;p&gt;The only difference is the target:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in Part 1, replay rebuilt a plain in-memory hashmap,&lt;/li&gt;
&lt;li&gt;now, replay rebuilds the current Memtable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the Memtable is sorted, those replayed writes are immediately ready for reads and for the next SSTable flush.&lt;/p&gt;

&lt;p&gt;After startup, the full picture is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recent writes: rebuilt from WAL into the Memtable,&lt;/li&gt;
&lt;li&gt;older writes: already stored in SSTables,&lt;/li&gt;
&lt;li&gt;SSTable index blocks: loaded into memory for reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;At this point, we have a working storage engine. WAL gives us durability. Memtable gives us fast in-memory writes with sorted ordering. SSTables give us efficient disk lookups using index blocks and binary search. And on startup, all the index blocks are pre-loaded so that reads only touch one candidate data block per SSTable checked.&lt;/p&gt;

&lt;p&gt;But several problems remain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The number of SSTable files keeps growing.&lt;/strong&gt; Every Memtable flush creates a new file. Over time, the number of files on disk keeps increasing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate keys accumulate across files.&lt;/strong&gt; If a key is updated 100 times, 100 different SSTable files may contain a value for it. Only the newest matters. The rest are wasted space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads slow down.&lt;/strong&gt; Every &lt;code&gt;Get()&lt;/code&gt; call may need to search through more and more files before finding the key (or confirming it doesn't exist).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deletes don't exist yet.&lt;/strong&gt; In an append-only system, what does "delete" even mean? We can't go back and erase a key from an already-written SSTable file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In upcoming parts, we will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compaction&lt;/strong&gt;: &lt;em&gt;to be covered in part 3&lt;/em&gt;: merging multiple SSTables into fewer, larger ones, eliminating duplicates and reclaiming space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tombstones&lt;/strong&gt;: a way to represent deletes in an append-only world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bloom filters&lt;/strong&gt;: a probabilistic data structure that lets us skip SSTable files that definitely don't contain a key, without reading anything from them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuning flush size and block size&lt;/strong&gt;: how large should the Memtable grow before flushing? How large should each data block be? These are not arbitrary choices and involve careful tradeoffs.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>algorithms</category>
      <category>computerscience</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building SaarDB, Part 1: Write-Ahead Log (WAL)</title>
      <dc:creator>Gagandeep Singh Ahuja</dc:creator>
      <pubDate>Sat, 04 Jul 2026 17:08:25 +0000</pubDate>
      <link>https://dev.to/gagandeepahuja09/building-saardb-part-1-write-ahead-log-wal-3245</link>
      <guid>https://dev.to/gagandeepahuja09/building-saardb-part-1-write-ahead-log-wal-3245</guid>
      <description>&lt;p&gt;SaarDB is a learning project: build a relational database from first principles in Go, one layer at a time.&lt;/p&gt;

&lt;p&gt;The goal is not to build a production database. The goal is to make database internals feel intuitive by building the core ideas directly and explaining the tradeoffs along the way.&lt;/p&gt;

&lt;p&gt;When we think of a relational database, it is tempting to start with SQL parsing, tables, data types, and query execution. But that starts with the interface before understanding the storage layer underneath it.&lt;/p&gt;

&lt;p&gt;So Part 1 starts smaller: a key-value store. From there, we will hit the first serious database problem: &lt;strong&gt;how do we make writes survive a crash?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start: Why a Key-Value Store?
&lt;/h2&gt;

&lt;p&gt;The initial instinct may be to start with the query layer. This includes SQL parsing, operations like &lt;code&gt;CREATE TABLE&lt;/code&gt;, multiple data types, and so on. But the storage layer is the foundation beneath all of that.&lt;/p&gt;

&lt;p&gt;The key insight is that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A key-value store can be extended to support everything a relational database needs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This can feel surprising at first. How can a relational database, with tables and rows and SQL, start from something as small as key-value operations?&lt;/p&gt;

&lt;p&gt;The reason for this is that every SQL operation can be mapped to a key-value operation. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CREATE TABLE payments (...)&lt;/code&gt; can be mapped to storing the schema as a value under key &lt;code&gt;_schema:payments&lt;/code&gt;. The value can be stored in a serialized manner and can be deserialized when we need to read it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INSERT INTO payments VALUES (1, 500, 'pending')&lt;/code&gt; can be mapped to a PUT operation: &lt;code&gt;PUT payments:1 &amp;lt;serialized_row&amp;gt;&lt;/code&gt;. This design also ensures efficient lookup by primary key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this mapping still feels abstract, that is fine. Later posts will come back to this when we build more of the SQL layer on top of the storage engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting Simple: An In-Memory Key-Value Store
&lt;/h2&gt;

&lt;p&gt;Whatever problem we are solving, we start with the simplest thing that works.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Correctness before performance. Performance before scale.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The simplest database is a hashmap in memory to support GET and PUT commands. We will build a REPL (Read-Eval-Print-Loop) around it so users can interact via the command line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the REPL
&lt;/h3&gt;

&lt;p&gt;A REPL is just a loop: accept input, process it, print the result, repeat. In Go, &lt;code&gt;bufio.Scanner&lt;/code&gt; reads from STDIN line by line:&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;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;:=&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bufio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SplitN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"PUT"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"EXIT"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"command not supported"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. We can store and retrieve data. But there is an obvious problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Problem: Memory Is Volatile
&lt;/h2&gt;

&lt;p&gt;If we kill the process and restart it, everything is gone. The hashmap lived in memory and memory does not survive process restarts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we make data survive crashes?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the problem of &lt;strong&gt;durability&lt;/strong&gt;: guaranteeing that once a write succeeds, the data is permanently saved.&lt;/p&gt;

&lt;p&gt;The answer is straightforward: write to disk. Memory is volatile. Disk is not. But this raises a new question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Write Order
&lt;/h2&gt;

&lt;p&gt;For every &lt;code&gt;PUT&lt;/code&gt;, we need two updates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write to disk so the data survives a crash.&lt;/li&gt;
&lt;li&gt;Write to the in-memory map so reads are fast.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The order matters.&lt;/p&gt;

&lt;p&gt;If we update memory first, there is a dangerous window where memory contains data that disk does not. If the process crashes in that window, the update disappears after restart.&lt;/p&gt;

&lt;p&gt;Even without a crash, if the disk write fails after the memory update, we now have to undo the memory change. That makes the write path more complicated.&lt;/p&gt;

&lt;p&gt;If we write to disk first, the rule is simpler: only update memory after the durable write succeeds.&lt;/p&gt;

&lt;p&gt;So the rule is: &lt;strong&gt;write to disk first, then update memory.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Append-Only Writes Help
&lt;/h2&gt;

&lt;p&gt;There is a golden rule for database performance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sequential writes are significantly faster than random writes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's go over what sequential and random writes actually are and why that is the case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential write&lt;/strong&gt; means that while writing data, we write in such a way that the data is stored in adjacent blocks on the storage drive. &lt;/p&gt;

&lt;p&gt;On the other hand, &lt;strong&gt;random write&lt;/strong&gt; means that the data is written in a scattered way across non-contiguous locations. &lt;/p&gt;

&lt;p&gt;For HDDs, random writes are slow because the disk head has to physically jump around. SSDs do not have a moving head, but the file-write pattern still matters.&lt;/p&gt;

&lt;p&gt;Consider two updates to the same key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice
PUT user_1 Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose the file already contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user_1 = Alice]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we want to update &lt;code&gt;user_1&lt;/code&gt; to &lt;code&gt;Bob&lt;/code&gt;. If we overwrite the old value in place, we first have to find the old bytes, check whether the new value fits in the same space, and handle the case where it does not.&lt;/p&gt;

&lt;p&gt;A simpler approach is to never rewrite old bytes. Just append the new value to the end of the file and treat the latest value as the source of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user_1 = Alice]
[user_1 = Bob]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The fastest possible write pattern is &lt;strong&gt;append-only&lt;/strong&gt;: always add data to the end of the file, never go back to modify earlier data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, we store the key twice. That is wasted space. But the write is fast, and we know the latest value is always at the end. We will deal with the duplication problem later.&lt;/p&gt;

&lt;p&gt;This append-only file is called a &lt;strong&gt;Write-Ahead Log (WAL)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoding WAL Entries
&lt;/h2&gt;

&lt;p&gt;We need to write a command like &lt;code&gt;PUT user_1 Alice&lt;/code&gt; to the file. The naive approach is to use a separator like a space or newline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice\n
PUT user_2 Bob\n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks readable, but it hides two different problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: Where Does One WAL Record End?
&lt;/h3&gt;

&lt;p&gt;Suppose the value contains a newline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice
Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If our WAL reader treats newline as the end of an entry, it will think &lt;code&gt;Bob&lt;/code&gt; is a separate command. So the first thing we need is a way to say: &lt;strong&gt;this WAL record is exactly N bytes long.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The solution is a length prefix around the whole WAL payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[wal_payload_length][wal_payload]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a concrete byte-level example. Suppose the WAL payload is &lt;code&gt;"PUT a 1"&lt;/code&gt; (7 bytes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[00 00 00 07][50 55 54 20 61 20 31]
 └─ length ─┘└──── WAL payload ────┘
     = 7        P  U  T     a     1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WAL reader knows: read 4 bytes for the length (7), then read exactly 7 bytes for the payload. This removes ambiguity about where this WAL record ends.&lt;/p&gt;

&lt;p&gt;In plain English: the first 4 bytes say, "the next chunk is 7 bytes long."&lt;/p&gt;

&lt;p&gt;That solves record boundaries. But it does not yet solve how to parse the command inside the payload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: How Do We Parse the Command Inside the Payload?
&lt;/h3&gt;

&lt;p&gt;If the payload itself is still a string like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we are back to the same problem. Is the value &lt;code&gt;Alice&lt;/code&gt;? Or is the value &lt;code&gt;Alice Bob&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;So the payload also needs length prefixes for its internal fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[cmd_len][cmd][key_len][key][value_len][value]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, &lt;code&gt;PUT user_1 "Alice Bob"&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[3][PUT][6][user_1][9][Alice Bob]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact bytes use 4-byte integers for the lengths, but conceptually this is the format. First we read the command, then the key, then the value. Spaces and newlines inside the value are just bytes; they do not confuse the parser.&lt;/p&gt;

&lt;p&gt;In Go, the write flow looks like:&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;func&lt;/span&gt; &lt;span class="n"&gt;appendLengthPrefixedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&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;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppendUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;serialisePutCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;:=&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;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appendLengthPrefixedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"PUT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appendLengthPrefixedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appendLengthPrefixedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of &lt;code&gt;buf&lt;/code&gt; as a growing byte slice: a resizable list of bytes. For each field, we first append its 4-byte length, then append the actual bytes.&lt;/p&gt;

&lt;p&gt;And for reading one length-prefixed field:&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;func&lt;/span&gt; &lt;span class="n"&gt;readLengthPrefixedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&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;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;valueLen&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueLen&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueLen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;offset&lt;/code&gt; is just a cursor. It remembers where the next field starts inside the byte slice.&lt;/p&gt;

&lt;p&gt;The important idea is that we use length prefixes at two levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The WAL layer frames the whole record.&lt;/li&gt;
&lt;li&gt;The database command layer frames the fields inside that record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern of &lt;code&gt;[length][data]&lt;/code&gt; shows up everywhere in storage systems. We will use it again in future blogs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Partial Writes
&lt;/h2&gt;

&lt;p&gt;Let's consider this scenario:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We start writing a 100-byte payload&lt;/li&gt;
&lt;li&gt;The OS writes the 4-byte length header: &lt;code&gt;[00 00 00 64]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The process crashes after writing only 50 bytes of the payload&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The file now contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...[valid entries]...[00 00 00 64][50 bytes of partial data]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the length prefix helps. Because the payload length is written before the payload, the reader knows exactly how many bytes to expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[wal_payload_length][wal_payload]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The writer side looks like this:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Wal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WriteEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="m"&gt;4&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PutUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;buf[0:4]&lt;/code&gt; stores the length header. &lt;code&gt;buf[4:]&lt;/code&gt; stores the payload right after it.&lt;/p&gt;

&lt;p&gt;The reader uses the length to read exactly the expected number of bytes:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Wal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ReadEntry&lt;/span&gt;&lt;span class="p"&gt;()&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;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;lengthBuf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lengthBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EOF&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EOF&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrUnexpectedEOF&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"partial write: incomplete length"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;payloadLength&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengthBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="n"&gt;payloadLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrUnexpectedEOF&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"partial write: incomplete payload"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the length says 100 bytes but only 50 bytes exist, &lt;code&gt;io.ReadFull&lt;/code&gt; returns &lt;code&gt;io.ErrUnexpectedEOF&lt;/code&gt;. That tells us the last WAL record was only partially written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Corrupted Bytes
&lt;/h2&gt;

&lt;p&gt;A partial write is about missing bytes.&lt;/p&gt;

&lt;p&gt;A corrupted write is different: all bytes may be present, but some bytes are wrong. This is not just a theory. Disks, SSDs, filesystems, OS crashes, power loss, and storage controllers can all fail in ways that leave bad bytes behind.&lt;/p&gt;

&lt;p&gt;Most writes work fine. But databases are built for the cases where bytes are missing or wrong. The length prefix catches missing bytes. It does not prove that the bytes we read are the correct bytes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How does the reader know the bytes are correct?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A checksum solves this. A checksum is a small fingerprint computed from data. If the payload bytes are the same, the checksum will be the same. If even one byte changes accidentally, the checksum will most likely change.&lt;/p&gt;

&lt;p&gt;For example, suppose the writer stores this payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The writer computes a checksum from these bytes and stores it next to the payload. Later, if the payload somehow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT user_1 Aljce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the length may still be valid, but the checksum will likely be different. That tells us the payload bytes are not trustworthy.&lt;/p&gt;

&lt;p&gt;Now we extend the same WAL record by adding a 4-byte checksum at the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[wal_payload_length][wal_payload][checksum]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This problem is commonly solved in storage systems using CRC32, which is a fast checksum algorithm. The writer computes the CRC32 checksum over the payload bytes and appends it. The reader reads the payload, independently computes the checksum, and compares it with the stored checksum.&lt;/p&gt;

&lt;p&gt;On the writer side, the length and payload logic stays the same. We only add space for the checksum, compute it from the payload, and append it:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Wal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WriteEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="m"&gt;4&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// ...same length and payload writes as before...&lt;/span&gt;
    &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PutUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// New part: append checksum after the payload.&lt;/span&gt;
    &lt;span class="n"&gt;checksum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;crc32&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChecksumIEEE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PutUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;checksum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buf[4+len(payload):]&lt;/code&gt; means "start writing after the 4-byte length header and the payload." That is where the checksum goes.&lt;/p&gt;

&lt;p&gt;With length prefixes and checksums together, the reader can handle these failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete length&lt;/strong&gt; (less than 4 bytes written): &lt;code&gt;io.ReadFull&lt;/code&gt; returns &lt;code&gt;io.ErrUnexpectedEOF&lt;/code&gt;. We know this is a partial write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete payload&lt;/strong&gt;: Length says 100 bytes but only 50 exist. Same error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete checksum&lt;/strong&gt;: Payload looks complete but checksum is truncated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrupted data&lt;/strong&gt;: All bytes present but checksum does not match. Data is bad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the reader has already read the length and payload, the checksum check is the extra part:&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="c"&gt;// ...same length and payload reads as before...&lt;/span&gt;

&lt;span class="n"&gt;checksumBuf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&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;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checksumBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrUnexpectedEOF&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"partial write: incomplete checksum"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;storedChecksum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BigEndian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checksumBuf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;computedChecksum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;crc32&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChecksumIEEE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;storedChecksum&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;computedChecksum&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"corrupt: checksum mismatch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intended recovery strategy is: if we detect a partial write at the &lt;strong&gt;end&lt;/strong&gt; of the file, we can truncate it because it was the last write that did not complete. In the current implementation, we detect and return the error first; truncating the partial tail is a follow-up cleanup. On the other hand, if corruption is detected mid-file, that is a more serious problem (bit rot, hardware failure) that requires manual intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Writes Actually Reach Disk
&lt;/h2&gt;

&lt;p&gt;There is one more subtlety. Consider this code:&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="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// process reports "write successful"&lt;/span&gt;
&lt;span class="c"&gt;// power goes out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did the data actually reach the disk? &lt;strong&gt;No, not necessarily.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When we call &lt;code&gt;file.Write()&lt;/code&gt;, the OS copies the data into a &lt;strong&gt;page cache&lt;/strong&gt; (a buffer in RAM). The OS returns success immediately. It will eventually flush the page cache to the physical disk, but "eventually" might be seconds or minutes later. If power fails before that flush, the data is gone despite &lt;code&gt;Write()&lt;/code&gt; returning success.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;file.Write()&lt;/code&gt; guarantees the data reached the OS. It does not guarantee the data reached the disk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The solution is &lt;code&gt;fsync&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="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// forces OS to flush page cache to physical disk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file.Sync()&lt;/code&gt; (which calls the &lt;code&gt;fsync&lt;/code&gt; system call) blocks until the OS confirms the data is on the physical storage device. Only after &lt;code&gt;Sync()&lt;/code&gt; returns, we can be confident that the data is durable.&lt;/p&gt;

&lt;p&gt;There is a tradeoff though. &lt;code&gt;fsync&lt;/code&gt; is expensive. On an HDD, it can take 5-10ms. On an SSD, it is faster but still significant. Calling it after every single write gives maximum durability but reduces throughput. Production databases like PostgreSQL batch multiple writes before a single &lt;code&gt;fsync&lt;/code&gt;, trading a small window of vulnerability for much higher throughput.&lt;/p&gt;

&lt;p&gt;For our database, we call &lt;code&gt;file.Sync()&lt;/code&gt; after every WAL entry. This means maximum safety and simpler reasoning but with performance tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Write Path
&lt;/h2&gt;

&lt;p&gt;Now we can put it all together. When a user calls &lt;code&gt;PUT key value&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serialize the command payload&lt;/strong&gt; into bytes: &lt;code&gt;[cmd_len][cmd][key_len][key][value_len][value]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append it as a WAL record&lt;/strong&gt;: &lt;code&gt;[wal_payload_length][wal_payload][checksum]&lt;/code&gt; and &lt;code&gt;fsync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insert into the in-memory hashmap&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Step 1-2: Write to WAL (disk first)&lt;/span&gt;
    &lt;span class="n"&gt;walPayload&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;serialisePutCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// Step 3: Write to memory&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WAL write happens first (disk before memory). If it fails, we return an error so that the user knows the write did not succeed. &lt;br&gt;
If the process crashes after the WAL write but before the in-memory map is updated, the data is safe on disk and will be recovered on restart.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Read Path
&lt;/h2&gt;

&lt;p&gt;Reads are simple right now. We just look up the in-memory map:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// key not found&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WAL is &lt;strong&gt;never read during normal operation&lt;/strong&gt;. It is purely a durability mechanism for writes. The in-memory map serves all reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Init: Rebuilding State from WAL
&lt;/h2&gt;

&lt;p&gt;When the database process starts, the in-memory hashmap is empty. But the WAL on disk contains every write since the last checkpoint. We need to replay it:&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;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;buildInMemoryMapFromWal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadEntry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EOF&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// reached end of WAL&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// corruption or partial write&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;readLengthPrefixedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"PUT"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;deserialisePutCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;only time the WAL is read&lt;/strong&gt;. The loop reads entries from oldest to newest. &lt;br&gt;
For duplicate keys, the newest value naturally overwrites the older one in the hashmap, which is exactly the behavior we want.&lt;/p&gt;

&lt;p&gt;After replay, the in-memory map contains the same state as before the crash. The database is ready to serve reads.&lt;/p&gt;

&lt;p&gt;The full initialization sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open WAL file&lt;/li&gt;
&lt;li&gt;Replay all WAL entries into a fresh in-memory map&lt;/li&gt;
&lt;li&gt;Start accepting reads and writes&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;WAL gives us durability and fast writes (append-only, sequential). Reads go through the in-memory hashmap.&lt;/p&gt;

&lt;p&gt;But we run into multiple issues at scale.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The WAL grows forever. Every update to the same key adds another entry, leading to a lot of redundant data.&lt;/li&gt;
&lt;li&gt;An in-memory hashmap gives us O(1) lookups but cannot scale beyond RAM.&lt;/li&gt;
&lt;li&gt;Replaying the entire WAL during application startup becomes painfully slow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In Part 2, we will introduce LSM trees, which solve these problems by adding sorted in-memory structures and searchable disk files.&lt;/p&gt;

&lt;p&gt;The code for SaarDB is available here:&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/gagandeepahuja09/saardb" rel="noopener noreferrer"&gt;https://github.com/gagandeepahuja09/saardb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>database</category>
      <category>go</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
