<?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: Tudor Stanila</title>
    <description>The latest articles on DEV Community by Tudor Stanila (@tudor96stani).</description>
    <link>https://dev.to/tudor96stani</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%2F3654211%2Fca14dac7-b94c-4e2f-990c-89b298c76d46.png</url>
      <title>DEV Community: Tudor Stanila</title>
      <link>https://dev.to/tudor96stani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tudor96stani"/>
    <language>en</language>
    <item>
      <title>Binary rows and AI</title>
      <dc:creator>Tudor Stanila</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:16:52 +0000</pubDate>
      <link>https://dev.to/tudor96stani/binary-rows-and-ai-414d</link>
      <guid>https://dev.to/tudor96stani/binary-rows-and-ai-414d</guid>
      <description>&lt;p&gt;Long time since an update here - I've been kept busy by various personal things, so involvement on this project has been lower than usual, but I should be getting more towards normal now. &lt;/p&gt;

&lt;p&gt;While I've done a few other things for the db engine, the binary row representation is the most relevant. &lt;/p&gt;

&lt;h1&gt;
  
  
  Binary rows
&lt;/h1&gt;

&lt;p&gt;Basically as said in previous posts, a data page will store a row/record as a contiguous chunk of bytes. Since the rows are variable length, we need to have an encoding for them as well. &lt;/p&gt;

&lt;p&gt;At its core, the format is compact and pretty clearly organized. We start with a header of course, then we have all the integers (fixed size), then a null bitmap then the string section (which will be variable length). Well, the integer section will be variable as well, depending on the number of integers, but at least we can easily split that one up. &lt;/p&gt;

&lt;p&gt;The overall layout is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| header | integers | NULL bitmap | string end offsets | string bytes |
 6 bytes   4 * I      8 bytes       2 * S               B bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;I&lt;/code&gt; is the number of integer columns&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;S&lt;/code&gt; is the number of string columns&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;B&lt;/code&gt; is the total number of UTF-8 bytes used by all string values&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Header
&lt;/h2&gt;

&lt;p&gt;The header itself is 6 bytes long and contains three &lt;code&gt;u16&lt;/code&gt; values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total row size&lt;/li&gt;
&lt;li&gt;integer column count&lt;/li&gt;
&lt;li&gt;string column count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the total row size works out to &lt;code&gt;6 + 4 * I + 8 + 2 * S + B&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integer storage
&lt;/h2&gt;

&lt;p&gt;Integer columns are stored in schema order, starting immediately after the header. The first integer starts at byte offset &lt;code&gt;6&lt;/code&gt;, and integer &lt;code&gt;j&lt;/code&gt; starts at &lt;code&gt;6 + 4 * j&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A NULL integer still occupies four bytes and is represented with zero in the payload; the bitmap tells us whether that value is actually NULL or just the integer zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  NULL bitmap
&lt;/h2&gt;

&lt;p&gt;The NULL bitmap is a fixed &lt;code&gt;u64&lt;/code&gt; value stored right after the integers. Bit 0 represents the first column, bit 1 the second, and so on. A set bit means NULL.&lt;/p&gt;

&lt;p&gt;The fixed-size bitmap is a design choice: it handles up to 64 columns, which is enough for now, and any unused bits stay zero.&lt;/p&gt;

&lt;p&gt;This is the first time I am implementing this concept - the Java version did not support NULL - so idk, things might have implementation wise. &lt;br&gt;
Though it was fun to do some bit shifting. &lt;/p&gt;
&lt;h2&gt;
  
  
  Strings and offsets
&lt;/h2&gt;

&lt;p&gt;Strings are stored after the bitmap as a sequence of &lt;em&gt;end offsets&lt;/em&gt; and then the UTF-8 payload itself.&lt;/p&gt;

&lt;p&gt;Each string column means one &lt;code&gt;u16&lt;/code&gt; in the end offset array. These offsets are relative to the beginning of the string-data section, not to the row itself. To find string &lt;code&gt;j&lt;/code&gt;, we look at its end offset and the previous string end offset. The first string starts at offset &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means if the end offsets are &lt;code&gt;[4, 17]&lt;/code&gt;, the first string occupies bytes &lt;code&gt;0..4&lt;/code&gt;, and the second occupies &lt;code&gt;4..17&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a nice encoding because it lets us find a value without scanning everything before it, while still keeping the data packed together. Other options (e.g. from the Java impl) were to store a pair of shorts for each string - &lt;code&gt;(start, length)&lt;/code&gt;. It allowed us not to check the previous entry to locate string &lt;code&gt;j&lt;/code&gt;, but it used more space. Decisions decisions.&lt;/p&gt;

&lt;p&gt;NULL and empty strings both contribute zero bytes to the string data section; the bitmap determines which one it is.&lt;/p&gt;
&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;For a schema 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;id    int
name  string
age   int
email string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and a row like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ id = 1, name = "Mary", age = 25, email = "mary@mail.com" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we have two integer columns and two string columns. &lt;code&gt;Mary&lt;/code&gt; uses 4 bytes and &lt;code&gt;mary@mail.com&lt;/code&gt; uses 13 bytes, so the row total is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6 + 8 + 8 + 4 + 17 = 43 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The row is laid out 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;row-relative range   contents
0..6                 header: 43, 2, 2
6..14                integers: 1, 25
14..22               bitmap: 0
22..26               string end offsets: 4, 17
26..43               blob: "Mary" followed by "mary@mail.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual bytes are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2B 00 02 00 02 00                         header
01 00 00 00 19 00 00 00                   integers
00 00 00 00 00 00 00 00                   bitmap
04 00 11 00                               string end offsets
4D 61 72 79                               "Mary"
6D 61 72 79 40 6D 61 69 6C 2E 63 6F 6D    "mary@mail.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why and how
&lt;/h2&gt;

&lt;p&gt;The implementation is pretty simple - just some public free floating functions. I kinda like the fact that I can do these - in Java I had to have a static class for these methods.&lt;br&gt;
Anyways, nothing special - just a function that receives a row &amp;amp; schema and produces a binary array if validation passes. Another one that does the opposite. Schemas are always passed for double checking. Rows by themselves, while they can be read and decoded, do not contain schema information (like column names or anything like that). &lt;br&gt;
It's pretty much the same design as I had in Java, with the NULL bitmap added - it worked pretty well there. &lt;/p&gt;

&lt;h1&gt;
  
  
  AI in the repo
&lt;/h1&gt;

&lt;p&gt;When I started this project I decided the limit the amount of AI I use - not cause I have anything against it, but because the goal of this project is for me to learn stuff and think things through, so having an agent write the implementation for me would have deprived me of those lessons - topic for another day, but I don't think you can actually learn something by having an AI do it. &lt;/p&gt;

&lt;p&gt;Anyways, the other goal for the project was to do it more 'correctly', as part of the &lt;em&gt;lessons learned&lt;/em&gt; from the Java attempt. Some of the top items on this list are: do proper testing and have proper documentation.&lt;/p&gt;

&lt;p&gt;And I still stand by those decisions - they will definitely help down the line.&lt;br&gt;
But this is also a pet project, done in my (more often than not) limited time. And it turns out that not only do they use a lot of time when there is only one person who has to handle them, but they also leave one quite tired.&lt;br&gt;
Perhaps that's one of the reasons I've not worked as much on this lately - whenever I opened the project, I was presented with the docs I had been working on previously, still in a draft state, still a long way to go. Or I would look at the method I had just written and realize I would need to spend at least an hour testing it and validating everything. &lt;br&gt;
Useful, but boring. &lt;/p&gt;

&lt;p&gt;So this weekend I've spent some time designing some agent skills to help me with that. The goal is still not to have AI write the code - but the help with the repetitive and uninteresting maintenance tasks, so I &lt;strong&gt;can&lt;/strong&gt; write the code myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Maintenance
&lt;/h3&gt;

&lt;p&gt;Started with some skills that handle technical docs - look at what's already there, what's implemented and keep the docs up to date. On demand or scheduled, depending on the situation. &lt;br&gt;
Also added some reviewer-style skills: mostly looking for gaps in docs, tests, reviewing any drifts in architecture, dependencies, tech debt, etc. &lt;br&gt;
One can be executed pre-PR: it checks the docs, checks the tests, does a review of the code changes to identify any obvious issues. Nothing fancy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements and testing
&lt;/h3&gt;

&lt;p&gt;Coming from the regulated field of medical software, I also thought I could apply some of the things from that way of working (simplifying things by making them more complex, I know.)&lt;/p&gt;

&lt;p&gt;Namely, one area that I knew from the beginning I would need, but did not feel like doing was requirements. And once you have those, testing becomes a bit easier (so does implementing).&lt;/p&gt;

&lt;p&gt;So as a next step, I implemented a few other skills with the following goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;turning rough ideas into precise requirements&lt;/li&gt;
&lt;li&gt;proposing test cases from those requirements&lt;/li&gt;
&lt;li&gt;reviewing for gaps in the test design&lt;/li&gt;
&lt;li&gt;implementing approved tests without drifting into implementation guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow is based on the following skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;requirements-engineer&lt;/code&gt; grills me about one topic/area (based on docs/what I give as input/domain knowledge) and proposes requirements for the desired implementation, stored in &lt;code&gt;docs/reqs.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;blackbox-test-designer&lt;/code&gt; produces tests from the requirements without reading the production implementation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;whitebox-test-designer&lt;/code&gt; looks at the code and proposes edge-case and unhappier flow scenarios&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test-implementer&lt;/code&gt; turns the approved cases into Rust tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them run automatically and the transition from each step to the next requires my review &amp;amp; approval. &lt;/p&gt;

&lt;p&gt;I did a small trial run on the &lt;code&gt;Schema&lt;/code&gt; component, and it was relatively useful. The grill-me part was probably the most useful and tbh I think the one that will be the most valuable in the future - partially cause it does write the reqs for me, but also cause it forces me to think of various edge cases, flows and other things I might have missed. The reqs it produced were ok - I had to do some tuning, but in the end they were ok. The test cases were also relatively ok - I had to tame it down since it ended up doing way too many for such a simple thing. &lt;br&gt;
The test cases implementation - I left this one sorta open in the skill, so that depending on the situation I can decide whether I want it to implement them as unit tests, public API tests, fully functional tests.&lt;/p&gt;

&lt;p&gt;This also does not mean I won't write any more tests or at least provide test cases, but I'll the the agent handle the large chunk of it - while I still think that they are important, a big chunk of them are relatively mechanic, throwing multiple inputs and checking the outputs. Also writing tests that verify the correct bytes are placed in the correct places in an array is not a super fun experience for me, not matter what.&lt;/p&gt;

&lt;p&gt;Haven't had the chance yet to determine if these will be useful or not, we'll have to see.&lt;/p&gt;

&lt;p&gt;During the following weeks I'll continue both with the implementation of new features and with some regression improvements - updating docs for existing functionality, adding requirements &amp;amp; tests. For as long as I still have credits in Codex I guess. &lt;/p&gt;

</description>
      <category>database</category>
      <category>dbms</category>
      <category>ai</category>
    </item>
    <item>
      <title>Server, crates and docs</title>
      <dc:creator>Tudor Stanila</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:15:44 +0000</pubDate>
      <link>https://dev.to/tudor96stani/server-crates-and-docs-1803</link>
      <guid>https://dev.to/tudor96stani/server-crates-and-docs-1803</guid>
      <description>&lt;p&gt;As I've had a pretty busy period, I haven't had the chance to post anything new here, and I only managed to get a few things done on the actual coding side - I will cover them here.&lt;/p&gt;

&lt;h1&gt;
  
  
  Server - client
&lt;/h1&gt;

&lt;p&gt;As the buffer implementation was thread safe (I hope!) from the beginning, I decided to avoid going the same route of implementing the whole thing as a single executable, only to have to split it into client and server later on.&lt;br&gt;
Therefore, I created a second executable for the database client (a CLI app) and implemented a TCP server on the main binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  High level
&lt;/h3&gt;

&lt;p&gt;The main idea behind the server implementation is the following: the app will use &lt;code&gt;tokio&lt;/code&gt; threads to asynchronously communicate via the socket with the client - this allows us to yield threads back to the pool as we are waiting for things like network.&lt;br&gt;
The more problematic area is actual query execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we do not want the client thread that accepted the connection to be blocked for the entire duration of the query execution&lt;/li&gt;
&lt;li&gt;we also do not want to sprinkle async through the entire codebase, but unless a long-running operation IS async (e.g. reading from disk), it will block the thread&lt;/li&gt;
&lt;li&gt;we do not want to give full control of the scheduling to the &lt;code&gt;tokio&lt;/code&gt; runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To accommodate this, the following implementation was chosen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only the handling of client requests is done on the async thread pool from &lt;code&gt;tokio&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;actual query execution is done on a worker, blocking thread&lt;/li&gt;
&lt;li&gt;we will control the number of concurrent active queries (active meaning being executed) through a fixed size worker thread pool&lt;/li&gt;
&lt;li&gt;we will, however, accept as many client connections/request as possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To achieve this, actual query execution is moved to the blocking thread pool from &lt;code&gt;tokio&lt;/code&gt;, operation which is guarded by a semaphore.&lt;/p&gt;

&lt;p&gt;The overall flow is the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Server boots up, reads config file (&lt;code&gt;trdb.toml&lt;/code&gt;) to determine data directory and buffer size. Also starts a TCP listener&lt;/li&gt;
&lt;li&gt;In a loop, calls &lt;code&gt;accept&lt;/code&gt; on the listener and hands off the interaction with the client on a separate &lt;code&gt;tokio&lt;/code&gt; thread (async) (actually does a &lt;code&gt;tokio::select!&lt;/code&gt; with an additional branch to listen for a shutdown request as well)&lt;/li&gt;
&lt;li&gt;The client handler starts a loop that reads (async) from the socket and prepares the query request - the client can send multiple requests on the same socket&lt;/li&gt;
&lt;li&gt;It attempts to enter the semaphore - if available, acquires a permit, otherwise it waits (done on a &lt;code&gt;tokio::select!&lt;/code&gt; as well, listening for a possible shutdown request)&lt;/li&gt;
&lt;li&gt;Once it passes the semaphore, it triggers a &lt;code&gt;spawn_blocking&lt;/code&gt; thread to actually execute the query and &lt;code&gt;awaits&lt;/code&gt; the result&lt;/li&gt;
&lt;li&gt;After it receives the result, it streams it back to the client.
### What works right now
While the overall logic of the server is implemented, it only does some very basic things for now:&lt;/li&gt;
&lt;li&gt;creates a page on startup, if it does not exist already on disk&lt;/li&gt;
&lt;li&gt;client handlers expect a single integer to be received&lt;/li&gt;
&lt;li&gt;it generates a row with that value repeated 100 times, inserts it in that given page, then reads it and sends it back&lt;/li&gt;
&lt;li&gt;a background worker is listening for shutdown requests, allowing us to add shutdown logic in the future (flushing buffer, etc)&lt;/li&gt;
&lt;li&gt;added logging as well &lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What is next
&lt;/h3&gt;

&lt;p&gt;Besides the obvious part of implementing a server that does more than accept an integer (and cleaning up the code which is all dumped in &lt;code&gt;main.rs&lt;/code&gt;), some next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define a TCP protocol for the client-server communication, to allow both inbound requests of queries and outbound responses with the result sets&lt;/li&gt;
&lt;li&gt;extract server related logic elsewhere&lt;/li&gt;
&lt;li&gt;make things more configurable (e.g. port)&lt;/li&gt;
&lt;li&gt;(nice to have) find a way to stream back the results as they arrive (so if the engine produces a total of 100k rows, start sending them 1k by 1k as the engine is still appending new ones to the result set, instead of waiting for it to return all of them at once)&lt;/li&gt;
&lt;li&gt;(nice to have) define a more custom scheduling policy for the worker threads - something towards a cooperative approach where the blocking worker threads can be returned to the pool while they are waiting for I/O.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Client
&lt;/h2&gt;

&lt;p&gt;The client app (&lt;code&gt;trdbcmd&lt;/code&gt;) is very simple as of right now. It only connects to the server, waits for user input of a number, sends it to the server and prints back the result. Obviously done only for testing purposes, will be expanded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;While many of the next steps are important, the current implementation allows me to test the things I am working on, so they have been pushed down in priority a bit.&lt;/p&gt;

&lt;h1&gt;
  
  
  Crates
&lt;/h1&gt;

&lt;p&gt;The other larger thing I have done was to reorganize the crates a bit.&lt;/p&gt;

&lt;p&gt;Initial choice for project structure was to separate each major component (storage engine, query engine, etc.) into separate small crates and define an &lt;code&gt;*-api&lt;/code&gt; crate (e.g. &lt;code&gt;storage-api&lt;/code&gt;) that acted as the gateway into that component. This might be too complex and overengineered.&lt;/p&gt;

&lt;p&gt;Because of this, I decided to simplify the architecture by doing a single crate/component. Therefore, I grouped all storage related crates into a single one (called &lt;code&gt;storage&lt;/code&gt;), adjusted the exposed items, error types, etc. This seems to have made things a bit cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docs
&lt;/h2&gt;

&lt;p&gt;These two last items are more maintenance: the first one is around docs:&lt;br&gt;
I started working on some actual technical docs, which are already behind schedule, as it's often the case with documentation. Will keep updating it as I go though.&lt;/p&gt;

&lt;p&gt;Additionally, I've decided to add my own notes to the repo - the ones I usually take during development (so they might be outdated or raw) - I think they be interesting from a timeline perspective. They can be found in &lt;code&gt;implementation_notes.md&lt;/code&gt;. They all have an ID (timestamp) and are pretty much pulled from my notebook unaltered. Since I am also placing them in my main Obsidian vault, they have some links that are not relevant in here.&lt;/p&gt;

&lt;p&gt;I've also added a &lt;code&gt;decision_log.md&lt;/code&gt; note in which i extract (from the actual implementation notes) any relevant decision I might make around the design.&lt;/p&gt;

&lt;p&gt;All of them can be found in the repo, under the &lt;a href="https://github.com/tudor96stani/trdb/tree/main/docs" rel="noopener noreferrer"&gt;/docs&lt;/a&gt; folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backlog and board
&lt;/h2&gt;

&lt;p&gt;I've also started defining the backlog of things that needs to be done - for the Java app I used GitHub's Project + Issues, but I did not particularly like it. For this one, I've created an Azure DevOps board (private) in which I transferred the backlog, organized it a bit.&lt;br&gt;
I still need to migrate the features from the Java board, right now I only have work items to reach feature parity.&lt;/p&gt;

</description>
      <category>database</category>
      <category>dbms</category>
      <category>rust</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Storage Manager - the buffer</title>
      <dc:creator>Tudor Stanila</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:14:31 +0000</pubDate>
      <link>https://dev.to/tudor96stani/storage-manager-the-buffer-16d</link>
      <guid>https://dev.to/tudor96stani/storage-manager-the-buffer-16d</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Having finished at least a preliminary implementation of the slotted pages, it is time to move on to the next part of the storage subsystem: its managers.&lt;br&gt;
In the original implementation, there were three of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File manager: handling the I/O with the file system&lt;/li&gt;
&lt;li&gt;Buffer manager: implementing the page buffer&lt;/li&gt;
&lt;li&gt;Storage manager: providing access into the storage system
The approach for Rust will be similar — the plan is to have the same three services. The lesson learned from Java being that the storage manager in particular can easily slip into having more responsibilities than it should. As a general rule, &lt;strong&gt;no service in the storage subsystem should deal with anything more granular than a page&lt;/strong&gt;. &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The buffer
&lt;/h2&gt;

&lt;p&gt;While the storage manager provides access to the entire module, the buffer manager is more interesting and far more important; therefore, I have started with it.&lt;/p&gt;

&lt;p&gt;The page buffer (or page cache) is one of the more important components of the engine. Because disk access is much slower than RAM, maintaining the active pages in an in-memory buffer is critical for the performance of the system. It is also an important player in the implementation of the whole &lt;em&gt;steal/no force&lt;/em&gt; ARIES algorithm and the entire logging &amp;amp; transactionality approach - none of which I will cover in this post, as they are complex topics that require their own separate analysis. &lt;/p&gt;

&lt;p&gt;For the purpose of this stage, though, the buffer manager plays a simpler role: caching data pages after they have been read from disk and providing callers access to them when requested. For now, we will keep to a minimum more advanced features like dirty page tracking, automatic flushing, pin counting and real eviction policies. These will be implemented later, the goal right now being to implement a buffer manager that is extensible, stable, and meets several requirements.&lt;/p&gt;
&lt;h2&gt;
  
  
  The requirements
&lt;/h2&gt;

&lt;p&gt;Before designing the buffer, I needed to clarify what the expectations of it are. They can be summarized as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the buffer should be the owner of the cached data pages and the only place in memory where the pages exist (i.e. do not allow page data to be cloned/duplicated/copied)&lt;/li&gt;
&lt;li&gt;callers request a page by its &lt;code&gt;PageID&lt;/code&gt; and receive a reference to the page data&lt;/li&gt;
&lt;li&gt;the buffer is responsible for negotiating with the &lt;code&gt;FileManager&lt;/code&gt; to trigger disk reads when a requested page is not found in the cache&lt;/li&gt;
&lt;li&gt;the implementation will allow concurrent access in a classic R/W locking mechanism:

&lt;ul&gt;
&lt;li&gt;read access to a page can be provided to any number of threads&lt;/li&gt;
&lt;li&gt;write access to a page is exclusive&lt;/li&gt;
&lt;li&gt;read/write access is controlled at the page level, not the entire buffer (multiple threads can obtain write access at the same time, provided they are working with different data pages)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The old design
&lt;/h2&gt;

&lt;p&gt;I started off with a design similar to the one in the Java implementation: the buffer contains a &lt;code&gt;HashMap&amp;lt;PageId, Page&amp;gt;&lt;/code&gt;. Given that no concurrent implementation was present in the Java version, this worked fine.&lt;/p&gt;

&lt;p&gt;This time around, besides the desire to design the buffer with concurrency in mind, I was also limited by the constraints imposed by the borrow checker. While defining a &lt;code&gt;HashMap&amp;lt;PageId, Page&amp;gt;&lt;/code&gt; is possible, there were several problems with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not thread safe - we would need some approach that allows concurrency:

&lt;ul&gt;
&lt;li&gt;if we only protected the pages (&lt;code&gt;HashMap&amp;lt;PageId, RwLock&amp;lt;Page&amp;gt;&amp;gt;&lt;/code&gt;), access to the map itself would still not be thread safe&lt;/li&gt;
&lt;li&gt;if we also protected the map (&lt;code&gt;RwLock&amp;lt;HashMap&amp;lt;PageId, RwLock&amp;lt;&amp;lt;Page&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;), we would also bottleneck access to it - no thread could access it to read page X while another thread was attempting to insert page Y&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;mutability - since even read operations can require mutation (in case of a cache miss, read from disk and insert in cache), the borrow checker will heavily block our attempts - every operation would require a mutable borrow of the map, which realistically is not doable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The new solution
&lt;/h2&gt;

&lt;p&gt;In order to solve all the problems caused by the old approach, I did the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate the task of caching pages from keeping track of what pages are cached&lt;/li&gt;
&lt;li&gt;use interior mutability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first one is relatively straightforward: instead of attempting to have a single data structure that both owns the pages and keeps track of them, I split the responsibilities over two structures: a pure cache and a page map.&lt;/p&gt;

&lt;p&gt;Interior mutability, a pattern that allows you to change the internal state of a type through a shared reference, is used in both structures and provides a way of updating the contents of the buffer manager via &lt;code&gt;&amp;amp;BufferManager&lt;/code&gt;, without the need to borrow it mutably (&lt;code&gt;&amp;amp;mut BufferManager&lt;/code&gt;). Since we are dealing with a multithreaded context, &lt;code&gt;Mutex&lt;/code&gt; and &lt;code&gt;RwLock&lt;/code&gt; (in combination with &lt;code&gt;Arc&lt;/code&gt;) were used. &lt;/p&gt;

&lt;p&gt;The following diagram shows the high level design:&lt;/p&gt;

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

&lt;p&gt;To achieve this desired result, a different approach was taken compared to the Java implementation: instead of thinking of the buffer as a collection in which we place new pages, we can see it as a pre-allocated chunk of memory in which pages are loaded from disk. This is done through a fixed-size block of memory allocated at startup and comprised of multiple &lt;strong&gt;frames&lt;/strong&gt;, each frame being able to store a single page. &lt;/p&gt;

&lt;p&gt;This has an important consequence: the cache structure itself is never mutated after construction. Its size stays constant regardless of the content it stores and how it is updated throughout its lifetime.&lt;/p&gt;

&lt;p&gt;Let's take a look at this part of the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BufferManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BufferFrame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BufferFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PageID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pin_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AtomicU32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dirty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AtomicBool&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;BufferFrame&lt;/code&gt; is a simple structure that &lt;strong&gt;owns&lt;/strong&gt; the data page and protects it with a &lt;code&gt;RwLock&lt;/code&gt;. This means that we never need to borrow the frame mutably in order to change the content of the underlying page. The same logic applies for the &lt;code&gt;PageID&lt;/code&gt;, the only difference being that the &lt;code&gt;Option&lt;/code&gt; allows us to flag that the frame is empty. &lt;/p&gt;

&lt;p&gt;Thanks to this, the buffer can only own a &lt;code&gt;Vec&amp;lt;BufferFrame&amp;gt;&lt;/code&gt;, without the need for additional thread protection. This is based on the fact that access to a &lt;code&gt;Vec&lt;/code&gt; is thread-safe as long as the vector itself is not mutated (which we're not doing): a &lt;code&gt;Vec&amp;lt;T,A&amp;gt;&lt;/code&gt; is &lt;code&gt;Sync&lt;/code&gt; if &lt;code&gt;T, A: Sync&lt;/code&gt; and &lt;code&gt;Send&lt;/code&gt; if &lt;code&gt;T, A: Send&lt;/code&gt; [2]&lt;/p&gt;

&lt;p&gt;At startup, a fixed-size &lt;code&gt;Vec&lt;/code&gt; is allocated, and each frame instantiated with default values: &lt;code&gt;None&lt;/code&gt; for &lt;code&gt;page_id&lt;/code&gt;, a zeroed-out page, &lt;code&gt;0&lt;/code&gt; for &lt;code&gt;pin_count&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; for &lt;code&gt;dirty&lt;/code&gt;.&lt;br&gt;
Worth noting, &lt;strong&gt;a zeroed-out page&lt;/strong&gt; does not mean the lack of one - in each frame, we have an instantiated &lt;code&gt;Page&lt;/code&gt; struct with all of its 4096 bytes set to zero.&lt;br&gt;
When a page is requested, it is read from disk &lt;strong&gt;directly&lt;/strong&gt; into the &lt;code&gt;frame.page.data&lt;/code&gt; byte array, thus populating the frame without mutating the overall data structure. &lt;br&gt;
&lt;strong&gt;Note&lt;/strong&gt;: the &lt;code&gt;page_id&lt;/code&gt; field of the frame is only used as an indicator that the frame is occupied, &lt;strong&gt;not&lt;/strong&gt; as a mechanism of keeping track of what pages are currently in the cache.&lt;/p&gt;
&lt;h3&gt;
  
  
  Page map
&lt;/h3&gt;

&lt;p&gt;Once we have sorted out how to have the buffer own and store the data pages themselves, we need a way to determine which pages we actually have in the cache. &lt;br&gt;
For this, we employ a new field and some data types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BufferManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;page_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PageEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PageEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PageState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cond_var&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Condvar&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;PageState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FrameId&lt;/span&gt;&lt;span class="p"&gt;)&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;FrameId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll break this down bottom-up:&lt;br&gt;
A frame is identified by its index in the &lt;code&gt;Vec&lt;/code&gt;, thus a &lt;code&gt;FrameId&lt;/code&gt; is just a nicer way of defining that &lt;code&gt;usize&lt;/code&gt; to make it clearer.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;PageState&lt;/code&gt; enum tells us whether the page is in the buffer, ready to be accessed at the given &lt;code&gt;FrameId&lt;/code&gt;, or if it is currently being read from disk. See the [Race conditions 2.1] for an explanation of why this is needed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;PageEntry&lt;/code&gt; is used as the value in the &lt;code&gt;page_map&lt;/code&gt;. The use of the &lt;code&gt;Mutex&lt;/code&gt; and &lt;code&gt;Condvar&lt;/code&gt; is also described in the [Race conditions 2.2] section. Suffice to say for both that they protect the resources during concurrent access.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;page_map&lt;/code&gt; field is a map from &lt;code&gt;PageID&lt;/code&gt; to its corresponding &lt;code&gt;PageEntry&lt;/code&gt;. Here, we do need to have a &lt;code&gt;RwLock&lt;/code&gt; protecting the map, as we will be mutating it during runtime: when a new page is loaded into a frame, we update the map with a new key-value pair.&lt;br&gt;
The value of the map is wrapped in an &lt;code&gt;Arc&lt;/code&gt; to limit the duration of the lock on the entire collection, allowing us to do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.page_table&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="nf"&gt;.get&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;page_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// map lock dropped here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By cloning the &lt;code&gt;Arc&lt;/code&gt;, we can drop the lock on the map and allow other threads to access it if needed. This way, even if we do need to lock the whole collection while mutating it, we can keep the locks as short as possible.&lt;/p&gt;

&lt;p&gt;Without the &lt;code&gt;Arc&lt;/code&gt;, &lt;code&gt;map.get&lt;/code&gt; would have given us a &lt;code&gt;&amp;amp;PageEntry&lt;/code&gt; that was tied to the guard from the &lt;code&gt;RwLock&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requesting pages
&lt;/h3&gt;

&lt;p&gt;So far we have covered the way the buffer is defined in terms of state, but what about behavior? How does this design affect the functional implementation?&lt;/p&gt;

&lt;p&gt;The three main operations I will be covering are: requesting a page to be read, requesting a page to be updated and requesting a new page. The buffer will, of course, offer more functionality in the future, but these are the important ones.&lt;/p&gt;

&lt;p&gt;The operations are defined as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="cd"&gt;/// Retrieves a page from the buffer pool based on its page ID.&lt;/span&gt;
    &lt;span class="cd"&gt;/// If the page cannot be found in the buffer, it is first loaded from disk, cached, then returned.&lt;/span&gt;
    &lt;span class="cd"&gt;///&lt;/span&gt;
    &lt;span class="cd"&gt;/// # Params&lt;/span&gt;
    &lt;span class="cd"&gt;/// - `page_id`: The identifier of the page to be retrieved.&lt;/span&gt;
    &lt;span class="cd"&gt;///&lt;/span&gt;
    &lt;span class="cd"&gt;/// # Returns&lt;/span&gt;
    &lt;span class="cd"&gt;/// A `Result` where the `Ok` contains a `PageReadGuard`. A `PageReadGuard` encapsulates the latch&lt;/span&gt;
    &lt;span class="cd"&gt;/// needed to access the underlying `&amp;amp;Page`.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PageReadGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BufferError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="cd"&gt;/// Retrieves a mutable page from the buffer pool based on its page ID.&lt;/span&gt;
    &lt;span class="cd"&gt;/// If the page cannot be found in the buffer, it is first loaded from disk, cached, then returned.&lt;/span&gt;
    &lt;span class="cd"&gt;///&lt;/span&gt;
    &lt;span class="cd"&gt;/// # Params&lt;/span&gt;
    &lt;span class="cd"&gt;/// - `page_id`: The identifier of the page to be retrieved.&lt;/span&gt;
    &lt;span class="cd"&gt;///&lt;/span&gt;
    &lt;span class="cd"&gt;/// # Returns&lt;/span&gt;
    &lt;span class="cd"&gt;/// A `Result` where the `Ok` contains a `PageWriteGuard`. A `PageWriteGuard` encapsulates the latch&lt;/span&gt;
    &lt;span class="cd"&gt;/// needed to mutably access the underlying `&amp;amp;Page`.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_page_mut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PageWriteGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BufferError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="cd"&gt;/// Finds a free frame and claims it for a new page with the given page ID.&lt;/span&gt;
    &lt;span class="cd"&gt;///&lt;/span&gt;
    &lt;span class="cd"&gt;/// Note that the frame might contain either a zeroed-page or a previous page that was flushed.&lt;/span&gt;
    &lt;span class="cd"&gt;/// Regardless, the caller is responsible for initializing the page and zero-ing it out.&lt;/span&gt;
    &lt;span class="cd"&gt;///&lt;/span&gt;
    &lt;span class="cd"&gt;/// # Params&lt;/span&gt;
    &lt;span class="cd"&gt;/// - `page_id`: the ID for the new page.&lt;/span&gt;
    &lt;span class="cd"&gt;///&lt;/span&gt;
    &lt;span class="cd"&gt;/// # Returns&lt;/span&gt;
    &lt;span class="cd"&gt;/// `PageWriteGuard` instance with write-access to the underlying page.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;allocate_new_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PageWriteGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BufferError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The methods return custom data types, defined as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PageReadGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// The underlying `RwLockReadGuard` which will be dereferenced to `&amp;amp;Page`&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLockReadGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;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;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Deref&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;PageReadGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PageWriteGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// The underlying `RwLockWriteGuard` which will be dereferenced to `&amp;amp;Page`&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLockWriteGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;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;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Deref&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;PageWriteGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DerefMut&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;PageWriteGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that each &lt;code&gt;BufferFrame&lt;/code&gt; protects the underlying &lt;code&gt;Page&lt;/code&gt; with a &lt;code&gt;RwLock&lt;/code&gt;. To get through the lock, you need to own a &lt;code&gt;RwLockReadGuard&lt;/code&gt; or &lt;code&gt;RwLockWriteGuard&lt;/code&gt;. Therefore, the buffer cannot hand out references to the &lt;code&gt;Page&lt;/code&gt; itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// if the buffer were defined as&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.find_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;frame_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 
    &lt;span class="c1"&gt;// a guard has been obtained in the line above, allowing access into the RwLock&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// the guard has been dropped here, so the &amp;amp;Page is no longer valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can, however, go into the frame, request a guard from the lock and return that to the caller. This way, as long as the caller keeps ownership of the guard, it has access to the page.&lt;br&gt;
And since this is a &lt;code&gt;RwLock&lt;/code&gt;, it automatically handles shared and exclusive access.&lt;br&gt;
The implementation of &lt;code&gt;Deref&lt;/code&gt; and &lt;code&gt;DerefMut&lt;/code&gt; means we can invoke methods of the &lt;code&gt;Page&lt;/code&gt; directly on the guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this design achieves
&lt;/h2&gt;

&lt;p&gt;If we look again at the requirements, we can see that this design satisfies them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;buffer is owner of a &lt;code&gt;Vec&amp;lt;BufferFrame&amp;gt;&lt;/code&gt;, where each &lt;code&gt;BufferFrame&lt;/code&gt; owns the page&lt;/li&gt;
&lt;li&gt;callers receive a guard that is dereferenced to a &lt;code&gt;&amp;amp;Page&lt;/code&gt; or &lt;code&gt;&amp;amp;mut Page&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;buffer reads pages into memory when they are not already cached&lt;/li&gt;
&lt;li&gt;concurrent R/W access to the pages is provided and thread safe

&lt;ul&gt;
&lt;li&gt;multiple &lt;code&gt;RwLockReadGuard&lt;/code&gt;s are allowed at once&lt;/li&gt;
&lt;li&gt;a single guard of type &lt;code&gt;RwLockWriteGuard&lt;/code&gt; is allowed&lt;/li&gt;
&lt;li&gt;multiple locks on different pages are allowed at the same time. The only serialization of operations is done when mutating the &lt;code&gt;page_map&lt;/code&gt;, but through &lt;code&gt;Arc&amp;lt;PageEntry&amp;gt;&lt;/code&gt; we can keep these locks as short as possible&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: here I have used the term locks multiple times. In reality, these would be more correctly called &lt;strong&gt;latches&lt;/strong&gt; - primitive synchronization mechanisms that control access to a physical resource. Down the line, we will implement a &lt;strong&gt;LockManager&lt;/strong&gt;, which will handle logical locks at object level during the execution of a transaction, while the latches will maintain the consistency of the in-memory objects and prevent corruption during multithreaded operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and lessons learned
&lt;/h2&gt;

&lt;p&gt;The implementation of the buffer is still in a very premature state - only the most basic of features have been done. However, I think it provides a pretty good foundation for what will come next. I’ve done a bit of testing (both automated tests and manual testing) and the concurrency seems to be handled correctly, allowing multiple threads to access the same page. &lt;/p&gt;

&lt;p&gt;One of the interesting aspects has been the difference in approaches I was forced to take compared to the Java implementation - in there, the buffer itself was a very direct implementation of a &lt;code&gt;HashMap&lt;/code&gt;, with little extra thought being given to it in terms of data structures or actual definition. Here, because the borrow checker did not allow me to just do the first thing that crossed my mind, I needed to stop for a moment and actually think about how I wanted this component to work - who should own the pages, where to give out references and what rules to apply to the entire flow. While this has taken a far longer time, I'm pretty confident the result is more robust. And I'm not saying this to preach Rust necessarily - the same solution could be done in any language, &lt;strong&gt;if you choose to do it&lt;/strong&gt;, whereas in Rust you are somewhat forced to do it. But coming from C#, this is still an interesting aspect for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Race conditions
&lt;/h3&gt;

&lt;p&gt;As mentioned, I identified a few possible &lt;strong&gt;edge case&lt;/strong&gt; race conditions that shaped the design:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Two threads look for an empty frame
&lt;/h4&gt;

&lt;p&gt;We must ensure that if two threads are searching for an empty frame in which to load a page, they do not choose the same one. &lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;&lt;br&gt;
When going through the frames, write-lock the one you find empty and set its &lt;code&gt;page_id&lt;/code&gt; from the start. This way, you can release the lock without worrying that others might use it before you get the chance to load the page.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.1 One right after the other, two threads want to read the same page
&lt;/h4&gt;

&lt;p&gt;The first thread sets the &lt;code&gt;page_id&lt;/code&gt; on the frame, but before it has the chance to load the page, the other attempts to read the page.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;&lt;br&gt;
To determine if a page is cached, always consult the &lt;code&gt;page_map&lt;/code&gt;, never the &lt;code&gt;frames.page_id&lt;/code&gt;. Additionally, publish in the &lt;code&gt;page_map&lt;/code&gt; that the page is available only after successfully loading it from disk.&lt;/p&gt;

&lt;h4&gt;
  
  
  2.2 Two threads want to load the same page in different frames
&lt;/h4&gt;

&lt;p&gt;Here, we need to ensure that the 2nd thread can differentiate between &lt;em&gt;page not in cache&lt;/em&gt; and &lt;em&gt;page being loaded into cache&lt;/em&gt;.&lt;br&gt;
&lt;strong&gt;Solution&lt;/strong&gt;&lt;br&gt;
The use of the &lt;code&gt;PageEntry&lt;/code&gt; with the two states (&lt;code&gt;Loading&lt;/code&gt; and &lt;code&gt;Ready(FrameId)&lt;/code&gt;) allows a thread to signal to others that it is currently reading the page from disk. It must insert the pair in the &lt;code&gt;page_map&lt;/code&gt; before starting the I/O and set it to &lt;code&gt;Loading&lt;/code&gt;. The second thread will notice that and wait on the &lt;code&gt;Mutex&lt;/code&gt; from the &lt;code&gt;PageEntry&lt;/code&gt;. Once the loader thread has finished reading the data from disk, it can update the state to &lt;code&gt;Ready(FrameId)&lt;/code&gt; and notify the waiting 2nd thread. Through the use of a &lt;code&gt;Condvar&lt;/code&gt;, the 2nd thread was not busy-waiting or consuming CPU while waiting for the event to occur.&lt;br&gt;
&lt;strong&gt;Note&lt;/strong&gt;: here, it is important in the 1st thread to obtain the read/write guard on the page before updating the map and notifying the waiters. Otherwise, the 2nd thread might wake up and snatch the lock on the page from the first thread which did all the work of reading the data from the disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage access times
&lt;/h3&gt;

&lt;p&gt;This table shows the access times for different storage/memory components, with an equivalent conversion in a more tangible unit of time. I find the table very relevant as it highlights just how slow disk reads are, especially compared to RAM and the CPU caches&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Equivalent duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1 cache&lt;/td&gt;
&lt;td&gt;0.5 ns&lt;/td&gt;
&lt;td&gt;0.5 sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2 cache&lt;/td&gt;
&lt;td&gt;7ns&lt;/td&gt;
&lt;td&gt;7 sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DRAM&lt;/td&gt;
&lt;td&gt;100ns&lt;/td&gt;
&lt;td&gt;100 sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSD&lt;/td&gt;
&lt;td&gt;150_000ns&lt;/td&gt;
&lt;td&gt;1.7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDD&lt;/td&gt;
&lt;td&gt;10_000_000ns&lt;/td&gt;
&lt;td&gt;16.5 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network storage&lt;/td&gt;
&lt;td&gt;30_000_000ns&lt;/td&gt;
&lt;td&gt;11.4 months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: [[1]]&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; Pavlo, A. (2022). &lt;em&gt;Database systems (15-445/645): 03 database storage part 1&lt;/em&gt;. Carnegie Mellon University. &lt;a href="https://15445.courses.cs.cmu.edu/fall2022/slides/03-storage1.pdf" rel="noopener noreferrer"&gt;Lecture slides&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Rust Project. (n.d.). &lt;em&gt;impl Send for Vec&lt;/em&gt; in &lt;em&gt;Vec Struct&lt;/em&gt; — Rust Standard Library documentation. Rust Programming Language. &lt;a href="https://doc.rust-lang.org/std/vec/struct.Vec.html#impl-Send-for-Vec%3CT%2CA%3E" rel="noopener noreferrer"&gt;Site&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>database</category>
      <category>dbms</category>
    </item>
    <item>
      <title>insertion in heap pages</title>
      <dc:creator>Tudor Stanila</dc:creator>
      <pubDate>Mon, 19 Jan 2026 16:44:52 +0000</pubDate>
      <link>https://dev.to/tudor96stani/insertion-in-heap-pages-3807</link>
      <guid>https://dev.to/tudor96stani/insertion-in-heap-pages-3807</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Insertion in heap pages is, in theory, straightforward: you find some free space in the page, place the row there and create a slot pointing to that location. There are, however, multiple small details that make things a bit more complicated (mostly edge cases caused by fragmentation). &lt;/p&gt;

&lt;p&gt;The implementation in TRDB is very similar to the one in Java - I mostly covered some scenarios I had missed there &amp;amp; cleaned it up a bit, making it more granular and easier to test. &lt;/p&gt;

&lt;p&gt;I will mostly cover the overall logic and reasoning behind it, limitations &amp;amp; possible improvements. &lt;/p&gt;

&lt;h2&gt;
  
  
  High level
&lt;/h2&gt;

&lt;p&gt;From a high level, the insertion is done in two separate steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plan the insertion&lt;/li&gt;
&lt;li&gt;perform the actual insertion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The planning phase looks for a location for the new row, along with metadata for the insertion algorithm. &lt;strong&gt;No data is changed during this phase, it is a read-only operation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The insertion phase uses the metadata provided in the plan to actually place the row at the target position and to update the header &amp;amp; slot array&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning phase
&lt;/h2&gt;

&lt;p&gt;This phase attempts to answer several questions about where the row is going to end up and what other changes are needed to the page from a maintenance point of view:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what slot number will the row use?&lt;/li&gt;
&lt;li&gt;where is it going to be physically placed?&lt;/li&gt;
&lt;li&gt;is compaction needed for this insert?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Slot number
&lt;/h3&gt;

&lt;p&gt;The first step of the planning is to go through the slot array and look for any unused slots. As a reminder: when a record is deleted its slot is only being invalidated, but kept in the array. The insertion algorithm will reuse these empty slots, to avoid increasing the footprint of the slot array unnecessarily. &lt;/p&gt;

&lt;p&gt;The scan starts from the beginning of the array (rightmost element) and stops at the first unused slot it encounters. If no such slot is found, it flags that a new one will be needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Physical location
&lt;/h3&gt;

&lt;p&gt;In the second step of the planning, we look for a place to physically place the row itself in the page. &lt;/p&gt;

&lt;p&gt;This is done by checking the following conditions, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can the row fit in the free space? (&lt;code&gt;free_end - free_start&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Can the row fit between two existing rows? &lt;/li&gt;
&lt;li&gt;Can the row fit between the last row and &lt;code&gt;free_start&lt;/code&gt;?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The options are probed one by one, halting at the first positive answer. If none are successful, the algorithm flags that a compaction is needed for this insertion.&lt;/p&gt;

&lt;h4&gt;
  
  
  Notes
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Prior to this phase, an additional check is done to ensure that the row can physically fit in the page - meaning that there are enough free bytes in the page, regardless of fragmentation; if there are not, the entire flow is aborted. This allows us to have the fallback of compaction (we can be sure that if we could not find a large enough spot right now, there will be one once we compact)&lt;/li&gt;
&lt;li&gt;All checks are done with the information from the first phase in mind - if a new slot is needed, 4 extra bytes are taken into account when determining the necessary overall space in the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;The actual result of the planning phase is represented as an enum comprised of two other enums &lt;a href="https://github.com/tudor96stani/trdb/blob/main/crates/storage/page/src/insertion_plan.rs" rel="noopener noreferrer"&gt;(code)&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Defines the offset at which a new record should be inserted in an unsorted heap page.  &lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Debug)]&lt;/span&gt;  
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;InsertionOffset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="cd"&gt;/// Record should be inserted at the start of free space after compacting the page.  &lt;/span&gt;
    &lt;span class="n"&gt;AfterCompactionFreeStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="cd"&gt;/// Record should be inserted at an exact offset.  &lt;/span&gt;
    &lt;span class="nf"&gt;Exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="cd"&gt;/// Defines whether a new slot should be created for the record or an existing slot can be reused when inserting into an unsorted heap page.  &lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Debug)]&lt;/span&gt;  
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;InsertionSlot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="cd"&gt;/// A new slot should be created for the record.  &lt;/span&gt;
    &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="cd"&gt;/// An existing slot can be reused for the record.  &lt;/span&gt;
    &lt;span class="nf"&gt;Reuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="cd"&gt;/// Represents a plan for inserting a new record into an unsorted heap page.  &lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Debug)]&lt;/span&gt;  
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;InsertionPlan&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="cd"&gt;/// The slot information for the insertion.  &lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InsertionSlot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="cd"&gt;/// The offset information for the insertion.  &lt;/span&gt;
    &lt;span class="k"&gt;pub&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;InsertionOffset&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;h2&gt;
  
  
  Insertion phase
&lt;/h2&gt;

&lt;p&gt;This is pretty straight forward: based on the insertion plan received from the previous step, it will know exactly whether it needs to compact the page beforehand, at what offset to place the row and how to handle the slot. &lt;/p&gt;

&lt;p&gt;Besides inserting the actual row data and updating the slot array, the following header values are also updated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SLOT_COUNT&lt;/code&gt; - incremented by 1 only if we are creating a new slot&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FREE_END&lt;/code&gt; - decremented by &lt;code&gt;SLOT_SIZE&lt;/code&gt; only if we are creating a new slot&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FREE_START&lt;/code&gt; - incremented by &lt;code&gt;row.size&lt;/code&gt; only if we are inserting at this location (meaning we are not using a gap between two rows)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FREE_SPACE&lt;/code&gt; - always updated. Decremented by &lt;code&gt;row.size&lt;/code&gt; if we are reusing a slot, or by &lt;code&gt;row.size + SLOT_SIZE&lt;/code&gt; if we are also creating a new slot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reasoning
&lt;/h2&gt;

&lt;p&gt;The initial implementation in Java was a single step insert - it worked ok, despite it becoming a bit too bloated. However, I had to split it into these two phase once I started working on write-ahead logging (WAL).&lt;/p&gt;

&lt;p&gt;As the name suggests, in WAL you need to create a log entry with the operation that is &lt;strong&gt;going to&lt;/strong&gt; happen, meaning you need to have all the information about the change you are about to make &lt;strong&gt;before&lt;/strong&gt; you perform the change. This was problematic because one of the fields that had to be included in the log entry was the slot number of the row being inserted - value which was only known during/after the insertion, not before. &lt;/p&gt;

&lt;p&gt;Since it is not acceptable to write an incomplete WAL entry and update it afterward, the only choice was to extract the logic that determined the physical location of the row outside the actual insertion, allowing me to (1) generate the insertion plan, (2) log the operation and (3) perform the actual insert. &lt;/p&gt;

&lt;p&gt;I could have, of course, updated the insert method to perform the log internally, right before it placed the row in the page. However, I preferred to have the &lt;code&gt;Page&lt;/code&gt; implementation unaware of the WAL and not couple the two in this manner. &lt;/p&gt;

&lt;h2&gt;
  
  
  Possible improvements
&lt;/h2&gt;

&lt;p&gt;During the search for a physical location in the planning phase, because the checks being performed are short-circuiting, their order is relevant and up for debate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on one hand the current implementation favors fragmentation, because it will only attempt to fill in gaps if the &lt;code&gt;free_end - free_start&lt;/code&gt; area is not large enough.  However, it avoids scanning the existing slots if it is not necessary, thus providing a quicker response.&lt;/li&gt;
&lt;li&gt;on the other hand, switching those two would reduce overall fragmentation, but would also incur useless scans of the slot array for scenarios where the fragmentation is low (which is also kept low by this algorithm)&lt;/li&gt;
&lt;li&gt;don't think there is a right or wrong, although implementing them both and doing some benchmarking would be the only way of telling for sure

&lt;ul&gt;
&lt;li&gt;it is also highly influenced by access patterns: tables with low deletions would benefit from the current implementation, while tables with lots of inserts &amp;amp; deletes could benefit from the second one&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Another area of improvement is also during the planning phase: as of right now, the two steps in the planning phase scan over the slot array twice: once for the slot # and once for the physical location. It can be done in a single pass, but for now I chose clarity and testability over efficiency. Again, only some benchmarks could give us a performance penalty for this implementation.&lt;/p&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>maintaining stable row identifiers during page maintenance</title>
      <dc:creator>Tudor Stanila</dc:creator>
      <pubDate>Mon, 05 Jan 2026 17:55:36 +0000</pubDate>
      <link>https://dev.to/tudor96stani/maintaining-stable-row-identifiers-during-page-maintenance-m9o</link>
      <guid>https://dev.to/tudor96stani/maintaining-stable-row-identifiers-during-page-maintenance-m9o</guid>
      <description>&lt;h2&gt;
  
  
  intro
&lt;/h2&gt;

&lt;p&gt;Christmas, New Year's, a 1-week ski trip, time with family, the rabbit hole of a full re-organization of my Obsidian vault into a &lt;a href="https://zettelkasten.de/overview/" rel="noopener noreferrer"&gt;Zettelkasten&lt;/a&gt; slip box after reading &lt;a href="https://www.soenkeahrens.de/en/takesmartnotes" rel="noopener noreferrer"&gt;How to take smart notes&lt;/a&gt; - all of these resulted in me making far less progress on this project than I would have expected, especially during my 3-week winter vacation. Nevertheless, there has been some progress.&lt;/p&gt;

&lt;p&gt;I am still within the storage system, working on the slotted pages for heaps and there have been a few interesting things I have encountered around the way a database engine will handle certain page maintenance operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  row IDs
&lt;/h2&gt;

&lt;p&gt;Most systems (though here I am mainly knowledgeable about how SQL Server handles things) will use a &lt;em&gt;row ID&lt;/em&gt; to internally identify a row.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;row ID&lt;/strong&gt; can be considered any identifier that allows the system to uniquely identify a single row. There can be multiple places where the engine would need to store this identifier, but maybe the best example is a non-clustered (or secondary) index. In here, each index entry will contain a pointer to the full row it belongs to. When retrieving rows based on that non-clustered index, the row identifier is used to obtain the full row (if needed).&lt;/p&gt;

&lt;h3&gt;
  
  
  how SQL Server does it
&lt;/h3&gt;

&lt;p&gt;In the case of SQL Server, row IDs used in non-clustered indexes depend on the source table, and it is usually referred to as the 'row bookmark value' (Delaney et al., 2013, p. 308):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if the source table is clustered, the non-clustered indexes will use the clustering key as the row identifier&lt;/li&gt;
&lt;li&gt;if the source table is a heap, the non-clustered index will use a physical row locator to identify the row&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today we are only focusing on heaps, as clustered tables are a whole other beast. That physical row locator (called RID by Microsoft) is an 8 byte value comprised of 3 different segments &lt;code&gt;FileID:PageID:SlotId&lt;/code&gt; and can be broken down into 2 bytes for the FileId, 4 bytes for the PageId and 2 bytes for the SlotId (Delaney et al., 2013, p. 311).&lt;/p&gt;

&lt;p&gt;The relevant point here: &lt;strong&gt;a row ID will contain the slot number of the row and this row ID will appear in all non-clustered indexes&lt;/strong&gt;. Which leads us to the concept behind this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  stable slot numbers
&lt;/h2&gt;

&lt;p&gt;Because indexes will use the slot ID as part of the row's identifier, the system needs to guarantee that rows maintain their slot number during internal page maintenance operations. If rows were to be assigned to other slots, all non-clustered indexes would also require an update, which would be pretty bad performance wise.&lt;/p&gt;

&lt;p&gt;The most direct implication of this rule is for the process of page compaction but, as I've noticed, the journey to guarantee this rule starts with deletions.&lt;/p&gt;

&lt;h2&gt;
  
  
  row deletions
&lt;/h2&gt;

&lt;p&gt;One of the advantages abstracting the access to rows through the slot array is that certain operations such as deletions become trivial. Similar to how a file system works, deletion from a slotted page does not require us to touch the actual row data. Instead, it is enough to invalidate the slot pointing to that row, thus making the area of bytes corresponding to the row completely unreachable. &lt;/p&gt;

&lt;p&gt;The process is quite simple, but what does it mean to invalidate a slot? In my case, it is just a matter of setting both its &lt;code&gt;offset&lt;/code&gt; and &lt;code&gt;length&lt;/code&gt; fields to &lt;code&gt;0&lt;/code&gt;, while keeping the slot count from the header untouched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initial: [(96, 100), (196, 50), (246, 50)]
slot_count = 3
// delete slot index 1 (196, 50)
after: [(96, 100), (0, 0), (246, 50)] 
slot_count = 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation adheres to the &lt;strong&gt;stable slot numbers&lt;/strong&gt; rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if, instead of invalidating the slot, we would have deleted it from the array completely (and decremented &lt;code&gt;slot_count&lt;/code&gt;), we would have had to shift the next slots one index down, thus blatantly breaking the rule&lt;/li&gt;
&lt;li&gt;if we set it to &lt;code&gt;(0,0)&lt;/code&gt; as we are doing now, but decremented the &lt;code&gt;slot_count&lt;/code&gt;, we would have made the last valid slot unreachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turns out this rule logically simplified the implementation by quite a lot. The only more complex part of deletion is a small optimization I've added: if the row being deleted is &lt;em&gt;physically&lt;/em&gt; the last on the page (so right at the end of the data section), we can actually avoid leaving the page fragmented by moving the &lt;code&gt;free_start&lt;/code&gt; pointer to the end of the previous row (physically). If this is the happy case, the &lt;code&gt;can_compact&lt;/code&gt; flag is also not set in the header to indicate fragmentation has started. &lt;/p&gt;

&lt;p&gt;I had actually missed doing this in the Rust implementation (despite me having done it in the Java db which was open on the other display) and I only realized it when I was trying to do the setup for some insertion tests - I initially marked the scenarios as 'technically possible, but not valid', until I gave it some more thought and discovered that they are in reality impossible &lt;em&gt;only if this optimization is implemented&lt;/em&gt; (you can still see the &lt;a href="https://github.com/tudor96stani/trdb/commit/182f42336fcb4195aeae1a95d7efc12df88eef88#diff-dfdbef1dd59ac775b8b2417ced94bb200da00cadb034183714effb8018c0003aR256" rel="noopener noreferrer"&gt;comment where I marked them as impossible&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  page compaction
&lt;/h2&gt;

&lt;p&gt;The process of internally reorganizing the records in the page, in order to remove fragmentation. While all pages start off as tightly packed arrays where all rows are in a contiguous chunk of bytes, deletions and updates can lead to small fragments of bytes appearing in between rows. As a result, a page's free space counter might be high, but without any contiguous area of space large enough to fit a new row. &lt;br&gt;
Compaction is done by moving all the existing rows next to each other, thus removing any fragments. &lt;br&gt;
It is usually a lazy process, performed when an insertion cannot find any large enough fragment to fit the new row.&lt;/p&gt;

&lt;p&gt;My implementation for it might not be the most optimal, but it seems to work. I allocate a new byte array the size of the data segment (&lt;code&gt;free_start - header_size&lt;/code&gt;), go through the slot array and for each &lt;em&gt;valid&lt;/em&gt; slot, copy the row in the new byte array, packing them one after the other &amp;amp; updating the slot offset value. Then I just overwrite the data segment with the new byte array (which will have some trailing 0s, as many as there were bytes from fragments).&lt;/p&gt;

&lt;p&gt;I'm sure there is some fancier in-place approach where you don't need to temporarily allocate a new array of up to 4k bytes, but that is more of a nice-to-have, later-on-if-i-ever-get-to-it optimization. Especially since this implementation checks the most important box: it does not break the rule of stable slot numbers. Funnily enough, the initial implementation in Java was to actually allocate a whole new page and only copy the valid slots, then replace the current page's byte array (a hacky way of leveraging the insertion algorithm without having to think too much). It was only while discussing the whole process with a friend almost a year later that we realized how the approach violated this rule. So maybe that's why I'm writing about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  outro
&lt;/h2&gt;

&lt;p&gt;That is mostly it for this post, which turned out to be way longer than I expected anyways. As I said in the beginning, things have been moving pretty slowly during the last few weeks, but they should pick up now that the holidays are over.&lt;/p&gt;

&lt;p&gt;I've also been working on is a nicer infrastructure for testing - my goal for this project is to try and add tests for everything as I am writing the code, to avoid having to cover tens of scenarios later on. I've been looking into ways in which I can leverage some of the features of Rust to create helpers for this module that will make the setup of any test scenario a breeze - but I think I will cover that in another post.&lt;/p&gt;

&lt;p&gt;One other thing that has kept me busy has been the insertion algorithm - the Java implementation was quite chunky, so I've been splitting it into smaller pieces, cleaning them up (and trying to cover them as much as possible).&lt;/p&gt;

&lt;p&gt;As next steps, I will hopefully finish the things related to heap pages pretty soon. Then I need to decide whether I want to stay in the storage engine and work on things like index pages (and the monstrous B+trees), or to expand into the other modules. &lt;br&gt;
Most likely I am going to go with the second. This whole process is quite slow, not due to the differences between the two languages, but because at every step I am questioning many of the implementation details from the Java db, trying to improve things or to solve shortcomings. Because of this, going through all the layers with a small working prototype might make decisions easier when I revisit them to complete the MVP. And on a subjective side of things, I'd like to have a working prototype as soon as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.microsoftpressstore.com/store/microsoft-sql-server-2012-internals-9780735658561" rel="noopener noreferrer"&gt;Delaney, K., Beauchemin, B., Cunningham, C., Kehayias, J., Freeman, C., Nevarez, B., &amp;amp; Randal, P. S. (2013). &lt;em&gt;Microsoft SQL Server 2012 Internals&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>Paging the data</title>
      <dc:creator>Tudor Stanila</dc:creator>
      <pubDate>Mon, 22 Dec 2025 05:21:21 +0000</pubDate>
      <link>https://dev.to/tudor96stani/paging-the-data-1hlf</link>
      <guid>https://dev.to/tudor96stani/paging-the-data-1hlf</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;At the foundation of any database engine is the storage layer - responsible for handling the actual persistence and retrieval of data on disk. In this post, I will go over database pages, the universally adopted abstraction for how the engine interacts with the disk. The theoretical part of the article does not propose any new design, but rather summarizes how databases typically structure and use pages. I then walk through my own implementation of database pages, which, while custom-built, follows closely the structure and behavior described in the theoretical overview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why pages
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;database page&lt;/strong&gt; (also referred to as a block) is the fundamental unit of storage and I/O inside most database engines. Instead of reading or writing arbitrary bytes, the database organizes its files into fixed-size blocks - generally 4 KB to 16 KB - that contain a header, records (rows) or index entries, and free space.&lt;/p&gt;

&lt;p&gt;Pages are the basis for how data is stored on disk, cached in memory, and manipulated by internal components such as B-trees, buffer pools, and write-ahead logging.&lt;/p&gt;

&lt;p&gt;Databases use pages because they provide deterministic control over I/O. Fixed-size blocks allow the engine to align with disk hardware, optimize sequential reads, minimize fragmentation and avoid expensive random access. This also makes caching predictable: a buffer pool can simply hold X frames, each corresponding to exactly one page, allowing clearer decisions about eviction, tracking dirty pages, and pinning.&lt;/p&gt;

&lt;p&gt;Pages also decouple the database from operating system memory management. Relying on the OS pages or the OS cache would make eviction, flushing, and write ordering unpredictable or outside the control of the engine. Databases need to know exactly when a block has been written, how to track its state relative to the write-ahead log, and how to restore it after a crash. Having their own page format and buffer management system gives them full control over these constraints.&lt;/p&gt;

&lt;p&gt;The page abstraction is also essential for data structures like B-trees and for space management within tables. Each index node fits within a single page, making lookups and modifications more efficient. More on B-tree backed indexes in a later post.&lt;/p&gt;

&lt;p&gt;As with many other areas and modules, the OS is the enemy of the engine, and the database will choose to implement its own mechanisms, rather than relying on the OS provided abstractions in order to be able to optimize and define its behavior exactly as it wants it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slotted pages
&lt;/h2&gt;

&lt;p&gt;The most common implementation for this is called a &lt;strong&gt;slotted page&lt;/strong&gt;. The reasoning behind this concept is explained in the &lt;strong&gt;Why slotted pages&lt;/strong&gt; section - but first I will quickly go over the structure and the behavior of this kind of page by using my design as an example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structure and behavior
&lt;/h3&gt;

&lt;p&gt;I have configured a page to be 4KB in size - I have chosen this size mostly because it is more manageable to look through while debugging and because I do most of my work on a Macbook with an SSD that has a 4KB block size as well, so that matches nicely.&lt;/p&gt;

&lt;p&gt;Pages are basically a contiguous chunk of 4096 bytes structured as a 96-byte header, immediately followed by a data region growing right (or down) and ending with a slot array growing left (or up):&lt;/p&gt;

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

&lt;h4&gt;
  
  
  Header
&lt;/h4&gt;

&lt;p&gt;The header represents the first 96 bytes of the page and stores metadata about the page itself: the total number of slots that are currently in use, the amount of free space remaining on the page, the type of the page (simple heap data page, index page, etc), various flags (e.g., fragmentation), pointers to the sibling pages (in the case of index pages).&lt;/p&gt;

&lt;p&gt;These are stored as different data types: for example, a &lt;code&gt;16 bit&lt;/code&gt; integer is enough to store the total number of rows in the page - we can never expect to have a large number of rows. Flags require even fewer bits, while other fields like sibling pointer might require more.&lt;/p&gt;

&lt;h4&gt;
  
  
  Slots
&lt;/h4&gt;

&lt;p&gt;The idea behind the slotted page is pretty simple: whenever you insert a row, you place it wherever there is some room in the page, then you create a new entry in the slot array - this entry will store the offset at which the row starts and its length. The row is now identified by its slot number.&lt;/p&gt;

&lt;h5&gt;
  
  
  Reading
&lt;/h5&gt;

&lt;p&gt;Because the slots are fixed size (4 bytes in my case - 2 for the offset, 2 for the length), it is trivial to read directly the &lt;em&gt;i-th&lt;/em&gt; slot - e.g. reading slot at index 3 simply means positioning your read-head at &lt;code&gt;PAGE_SIZE - (slot_number + 1) * SLOT_SIZE = 4096 - (3 + 1) * 4 = 4080&lt;/code&gt;, then reading the first 4 bytes (in the image above, that would give us the slot &lt;code&gt;(140, 12)&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Iterating over all the rows in the page is equally simple - you just iterate over the slot array, dereference each slot and output the corresponding row.&lt;/p&gt;

&lt;h5&gt;
  
  
  Updating
&lt;/h5&gt;

&lt;p&gt;As said, inserting a new row means finding some free space in the page and creating a new slot.&lt;/p&gt;

&lt;p&gt;Deletion, on the other hand, is straightforward - you can just delete (or invalidate) the slot - deleting the actual bytes of the row is optional and can be postponed, since that segment of the page is no longer accessible and will eventually be overwritten. A process called compaction will rearrange all the existing rows in a contiguous segment, removing any fragmentation that may have occurred after deletions.&lt;/p&gt;

&lt;p&gt;As for updating, if the new row has the same size (or smaller), the update can be done in place. If it's greater, the updated value will need to be placed somewhere else in the page and the slot's offset will be updated - the slot number, however, will remain unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why slotted pages
&lt;/h3&gt;

&lt;p&gt;The way I see it, slotted pages solve two main problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How to handle variable length rows&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to encapsulate storage details from outside objects&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Variable length rows
&lt;/h4&gt;

&lt;p&gt;Say we didn't have slotted pages, but wanted to store rows that vary in length. How would we do that? The most direct approach would be to store them as &lt;code&gt;[ row_length | row_bytes]&lt;/code&gt;- e.g. &lt;code&gt;[ 5 | 'hello']&lt;/code&gt;. This allows you to know exactly how many bytes you need to read to obtain a single row.&lt;/p&gt;

&lt;p&gt;But what about random access? What happens if you need to read the 3-rd row? You have no idea where it starts exactly, so you need to: read the length of row 1, move the cursor at &lt;code&gt;current + row_1_length&lt;/code&gt;, read the length of row 2, move the cursor at &lt;code&gt;current + row_2_length&lt;/code&gt;, then finally read the row you need.&lt;/p&gt;

&lt;p&gt;This can become cumbersome and inefficient.&lt;/p&gt;

&lt;p&gt;With slots on the other hand, because they are fixed size, you can determine exactly where the 3rd slot is stored, which will point you to where the row itself is stored. In our case, the 3rd slot (&lt;code&gt;index = 2&lt;/code&gt;) will be placed at &lt;code&gt;PAGE_SIZE - (index + 1) * SLOT_SIZE = 4096 - 3 * 4 = 4084&lt;/code&gt;. By computing this offset and setting the read head at that location, we are able to directly read the two &lt;code&gt;short&lt;/code&gt;s that compose the slot.&lt;/p&gt;

&lt;h4&gt;
  
  
  Encapsulating the details
&lt;/h4&gt;

&lt;p&gt;The other advantage of slots is that it allows you to hide from the outside world the way the page organizes itself underneath.&lt;/p&gt;

&lt;p&gt;Again, say we didn't have the slot array. If you look at the drawing of the page from the start of the post, you might notice that row 3 is missing because it was deleted. How would we handle the page operations now, say, for reading row #4? We would read row 1, then row 2 - and then? We would need some sort of pointer to indicate that we need to skip some bytes to be able to get to what is labelled as row 4, but in reality, is the now 3rd row. Or we would need to compact (remove fragmentation) on each delete.&lt;/p&gt;

&lt;p&gt;But if we were to compact on each delete - this means that row 4 now becomes row 3. This can become quite problematic. What if there is an index out there for this table? We would need to not only remove the old row from it, but also update rows 4, 5, 6, ... to become rows 3, 4, 5, ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem is that the logical ordering of the rows is dependent on the physical ordering and organization on disk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is very limiting and causes dependencies left and right. Even if external objects don't know the exact offset of the row, they are dependent on the place where it is stored and rows themselves are dependent on one another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slots allow us to hide all of these details from the outside world.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By only exposing the slot numbers to external objects, we are free to organize the rows however we want - fragmented, compacted, in increasing or decreasing order of their size, all even and all odd, does not matter because the slot array stays constant. Deleting a row and compacting the page means moving around the &lt;strong&gt;only the row data&lt;/strong&gt; - the slots stay unchanged. If you consult the drawing again, you will notice that the 3rd slot is &lt;code&gt;(0,0)&lt;/code&gt; - that is because the row was deleted and the slot invalidated, but not removed from the array. This means that after a compaction, the space that was occupied by the row will be overwritten, but the &lt;code&gt;(0,0)&lt;/code&gt; slot will remain in the array.&lt;/p&gt;

&lt;p&gt;As a result, row 4 will remain row 4, regardless of where it is stored in the page, so external objects like indexes will not be affected by any internal reorganization of the page.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Java
&lt;/h3&gt;

&lt;p&gt;The original Java implementation defined a class called &lt;code&gt;SlottedPage&lt;/code&gt; which contained a &lt;code&gt;ByteBuffer&lt;/code&gt; of size 4096 with the actual content from disk. It also exposed numerous methods for interacting with the page - reading and writing flags from the header, retrieving, inserting or updating rows, etc. Additionally, a &lt;code&gt;SortedPage&lt;/code&gt; class extended the &lt;code&gt;SlottedPage&lt;/code&gt; and offered overrides for B-tree index nodes.&lt;/p&gt;

&lt;p&gt;This grew a bit cumbersome for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Inheritance sometimes sucks - had a bug I spent several days fixing, only because I was expecting an object to be a &lt;code&gt;SortedPage&lt;/code&gt;, but was in reality an unsorted &lt;code&gt;SlottedPage&lt;/code&gt;, which broke some B-tree invariants. This could have been prevented by a better implementation or by paying more attention, but I feel my point still stands.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The classes grew quite big - exposing a different method for every header value, every behavior - this led to large objects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(A me problem) Not a lot of attention paid to data duplication - there were flows where the main byte array of the page was duplicated and copied around as it was passed from method to method.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rust
&lt;/h3&gt;

&lt;p&gt;The new implementation is pretty similar conceptually to the Java one. However, in a Rust-centric approach I tried to focus more on the ownership side of things and ensure that the data is not duplicated in memory for no good reason. I also leveraged the fact that you can have multiple &lt;code&gt;impl&lt;/code&gt; blocks across different files for the same &lt;code&gt;struct&lt;/code&gt; - this allowed me to group methods in different files based on their functionality.&lt;/p&gt;

&lt;p&gt;The next question was how to actually store the page bytes in memory and how it would play along with the buffer. The way I saw it, I had two main possible directions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The page owns the byte array, while the buffer cache owns the pages via some data structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The buffer manager allocates and owns a pool of frames where each page is placed -&amp;gt; then the page is only a pointer to that memory region.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Based on my research, the second option seems more popular or idiomatic. I chose, however, to go with the first one because it felt easier to avoid doing all that pointer arithmetic on the buffer pool &amp;amp; dealing with lifetimes. In a way it also felt more modern &amp;amp; more Rust (might be wrong here tho).&lt;/p&gt;

&lt;p&gt;The base of it all is the &lt;code&gt;Page&lt;/code&gt; struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageId&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="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;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;page_id&lt;/code&gt; is just an in-memory page identifier comprised of &lt;code&gt;filename_hash::page_number_within_file&lt;/code&gt;. Only the page number within the file is stored in the page header - the filename itself will be known through the use of system catalogs. The &lt;code&gt;data&lt;/code&gt; field stores the main byte array of the page in a &lt;code&gt;Box&lt;/code&gt; on the heap. I chose an actual array over something like a &lt;code&gt;Vec&lt;/code&gt; as the size will never change so the overhead of the &lt;code&gt;Vec&lt;/code&gt; seemed unnecessary.&lt;/p&gt;

&lt;h4&gt;
  
  
  HeaderRef and HeaderMut
&lt;/h4&gt;

&lt;p&gt;The first order of business was to provide access to the header of the page. Instead of going with a set of getters and setters on the main page, I decided to create two new structs, &lt;code&gt;HeaderRef&lt;/code&gt; and &lt;code&gt;HeaderMut&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;HeaderRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;HEADER_SIZE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;HeaderMut&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;HEADER_SIZE&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;These structures borrow from the main page byte array a slice the size of the header - namely the first 96 bytes. &lt;code&gt;HeaderRef&lt;/code&gt; offers an &lt;strong&gt;immutable view&lt;/strong&gt; into the header of the page via getters, while &lt;code&gt;HeaderMut&lt;/code&gt; offers a &lt;strong&gt;mutable view&lt;/strong&gt; via getters and setters. The word &lt;strong&gt;view&lt;/strong&gt; here is relevant to me - that is because these structs are zero-copy - they do not duplicate the underlying byte array in any way. Because their slices are also limited to the header size, they cannot affect the rest of the page, should there be any bug. I also decided to split them in mutable and immutable to keep the trend of Rust - if you want to modify something, you should opt in for the mutable one.&lt;/p&gt;

&lt;p&gt;The getters and setters are implemented via a macro that takes as param the flag/value name and data type, to avoid having to write a large number of methods manually.&lt;/p&gt;

&lt;h4&gt;
  
  
  SlotArrayRef and SlotArrayMut
&lt;/h4&gt;

&lt;p&gt;Similarly to the header, I also provide access to the slot array via the following structs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SlotArrayRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   
    &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SlotArrayMut&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;    
    &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&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 reasoning here is the same: slice into the main array and encapsulate behavior in a more organized and testable way. These structs are instantiated with a slice the size of the slot array at the time of its creation: if the page contains 3 slots, the slice will be &lt;code&gt;3*4&lt;/code&gt; bytes, or more precisely the last 12 bytes of the page.&lt;/p&gt;

&lt;p&gt;They offer methods for retrieving slots, and in the case of &lt;code&gt;SlotArrayMut&lt;/code&gt;, insert/update slots. They also expose the slot structs - a zero-copy view of a single slot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SlotRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;slot_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;SLOT_SIZE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SlotMut&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;slot_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;SLOT_SIZE&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;h4&gt;
  
  
  Page API
&lt;/h4&gt;

&lt;p&gt;With the exception of the main &lt;code&gt;Page&lt;/code&gt; struct, all behavior and structs are internal to the &lt;code&gt;page&lt;/code&gt; crate. The page exposes a set of public methods that are accessible outside of the crate - for creation of the page itself and for retrieval/insertion/deletion/update of rows. Underneath, these public methods leverage this structs and encapsulate the behavior of the page. The main &lt;code&gt;impl&lt;/code&gt; blocks are split into separate files: one for reading rows, one for inserting, etc. All of these internal methods can be tested in place where needed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Error handling
&lt;/h4&gt;

&lt;p&gt;I tried to create an error enum for every main flow: &lt;code&gt;InsertError&lt;/code&gt;, &lt;code&gt;ReadRowError&lt;/code&gt;, etc. Underneath, these are derivable for more concrete errors: all header operations can return &lt;code&gt;HeaderError&lt;/code&gt;, all slot operations can return &lt;code&gt;SlotError&lt;/code&gt;, etc. All of these errors can be converted to a &lt;code&gt;PageOpError&lt;/code&gt;, which represents the crate's catch-all error type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                       HeaderError
                      /
PageError - PageOpError - SlotError
                      \
                       \-ReadRowError
                        \
                         InsertError
                         ....
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, the &lt;code&gt;PageError&lt;/code&gt; is the crate's public error type and the only one exposed to the outside world. It is a struct containing a &lt;code&gt;PageOpError&lt;/code&gt; and an additional &lt;code&gt;PageId&lt;/code&gt;. A trait to more easily attach the page id is also implemented:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PageError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageOpError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;WithPageId&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_page_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PageResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WithPageId&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PageOpError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;with_page_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PageId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PageResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;PageError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&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;h4&gt;
  
  
  Remaining work
&lt;/h4&gt;

&lt;p&gt;At the time of writing this, only the insertion and retrieval of rows has been implemented (besides the header &amp;amp; slot array accessors). I started by taking the Java implementation as-is and translated it to Rust and right now I am trying to improve it by (1) splitting it into more granular and testable chunks, (2) optimizing and improving the overall logic. After that, I will apply the same principle to compaction, deletion &amp;amp; updates.&lt;/p&gt;

&lt;p&gt;Worth noting tho, this only covers heap unsorted pages. The B-tree index pages are a whole other thing that will be tackled separately, but that might be pushed a bit further down the priority list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;These resources helped inform my understanding of database page architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/pages-and-extents-architecture-guide?view=sql-server-ver17" rel="noopener noreferrer"&gt;Microsoft SQL Server Documentation — Pages and Extents Architecture Guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kalen Delaney — Microsoft SQL Server 2012 Internals&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.youtube.com/playlist?list=PLSE8ODhjZXjbj8BMuIrRcacnQh20hmY9g" rel="noopener noreferrer"&gt;CMU Intro to databases&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>So I decided to write a database system</title>
      <dc:creator>Tudor Stanila</dc:creator>
      <pubDate>Mon, 22 Dec 2025 05:18:59 +0000</pubDate>
      <link>https://dev.to/tudor96stani/so-i-decided-to-write-a-database-system-5483</link>
      <guid>https://dev.to/tudor96stani/so-i-decided-to-write-a-database-system-5483</guid>
      <description>&lt;p&gt;About a year ago I started a personal project to implement a relational database system from scratch, mostly as a result of (1) my interest in the field and (2) a desire to better understand how things work under the hood.&lt;/p&gt;

&lt;p&gt;Professionally, I’ve been working for the past 7.5 years as a .NET backend engineer. A lot of my recent tasks involved attempting to improve the response time of a SQL Server database, which led me to spend quite some time looking at queries, execution plans, and schema design, trying to find the culprit behind the performance issue. I kept reading articles and blogs about random operators and how they work to try and grasp what was happening to my query.&lt;/p&gt;

&lt;p&gt;The idea of writing some sort of database &amp;amp; query engine kept bouncing around in my head more like a conceptual thing - &lt;em&gt;I wonder how difficult it would be to implement a JOIN&lt;/em&gt; - until one day last fall when I was telling a friend about this over FaceTime, and we decided to make a go of it. We chose Java over something like C++ mostly because (a) it was a language we are both familiar with, C# being quite similar in many aspects, and (b) writing a database engine is hard enough - we wanted to focus on the design and functionality, not on also getting familiar with a low-level language at the same time.&lt;/p&gt;

&lt;p&gt;After one year of active development, I started feeling a bit stuck. Some of the design choices I made turned out not to be the best, which led me to create several tasks tagged with &lt;code&gt;refactor&lt;/code&gt; and &lt;code&gt;XXL&lt;/code&gt;. Looking at the impact these refactors would bring, it felt easier to cut my losses, learn from my mistakes, and re-work everything from scratch rather than make the changes in place.&lt;/p&gt;

&lt;p&gt;That decision also meant choosing a new direction for the implementation itself - not just a redesign, but a complete rewrite. I still didn't particularly want to go the C++ route, but I did want something closer to how actual database systems are implemented, so I chose to learn Rust for this new version.&lt;/p&gt;

&lt;p&gt;Since I’m redoing the whole thing anyway, I figured I might as well make use of everything I learned the first time around. One of the lessons was that I didn’t do a great job documenting my thought process or why I made certain choices. This time I want to change that, and having a place to write things down from the very beginning might help.&lt;/p&gt;

&lt;p&gt;This blog will follow my journey of learning Rust by re-implementing my initial database server. I’ll cover design choices and approaches I’ve taken for the engine, along with Rust-specific things I’ve used to implement them (or that I find cool). I know I’ll make plenty of mistakes - both Rust-specific and DB engine ones - but I guess that’s the whole point: I’ve been programming for long enough to know that you rarely get the right solution on the first attempt. I don’t regret any of the wrong choices from the Java project - by having made them, I’ve learned a lot more. Same goes for the &lt;em&gt;way&lt;/em&gt; I’ve implemented things in the few hundred lines of Rust code I’ve already written for this - I’m sure many of them will come back and bite me a few months from now, but that is part of the process I’ve learned to accept and actually enjoy.&lt;/p&gt;

&lt;p&gt;I use various resources when designing the system: lectures I find on YouTube from various universities, several books, documentation, and (very little) open source code like PostgreSQL or SQLite. Many of the design choices are influenced by SQL Server (through observation &amp;amp; Microsoft’s public documentation) mostly because it’s the system I’m most familiar with and enjoy the most. However, I try to use these resources only to guide me in the right direction and have the actual implementation be as much my own as possible.&lt;/p&gt;

&lt;p&gt;This database project will never see the light of day as an actual product to be used in real life - I don’t see the point in trying to create a competitor for existing solutions with decades of experience on the market (especially as a side project done by one person after work). It’s designed for my own growth and passion.&lt;/p&gt;

&lt;p&gt;I do welcome any kind of feedback from anyone reading this - whether I’ve made a stupid choice regarding the design/behavior of the database engine, or if I’ve written some junk piece of Rust code that can be improved. Appreciation of anything that looks right is also more than welcome :D&lt;/p&gt;

&lt;p&gt;The repo with the Rust code can be found &lt;a href="https://github.com/tudor96stani/trdb" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;code&gt;cargo&lt;/code&gt; generated docs can be found &lt;a href="https://tudor96stani.github.io/trdb/trdb/index.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
