SimpleDB (Part 1): File Manager
Contents
Every time you query a database, a complex series of actions begin behind the scenes. I’d like to peek behind the curtain and understand how databases work internally.
Recently I’ve been reading Edward Sciore’s Database Design and Implementation. In this series, I’ll try to answer this question using a Rust implementation of SimpleDB.
What we’ll cover
In this post in particular, we’ll build the foundation of a database system by implementing two core components: file management and page handling.
Please see the repo for the full implementation.
note: I am beginner in rust, so if you see anything that needs improvement, please let me know.
Database storage
There are two ways a database system could potentially access data. If you think of it like a library:
- block-level access is like going directly to a specific shelf and picking up a specific volume
- file-level access is like working with entire sections of the library at once
In a block-level interface, there is the concept of a block
, which is mapped to several sectors of the disk. In order to modify the disk:
- the sector contents of the block are read into a page
- bytes are modified on the page
- OS then writes the page back into the block on disk
On the other hand, a file-level interface is a higher level abstraction. The client views the file as a sequence of bytes, with no notion of a block. You can also read/write any number of bytes starting at any position in the file.
Most database engines use a compromise. They store all their data in one or more OS files, and treats each file as a raw ‘disk’. The database engine will access each ‘disk’ using logical file blocks. A logical file block tells you where the block is with respect to the file, but not where the block is on the disk. In comparison to a physical block reference that tells you where the block is on the disk.
The OS takes on the responsibility of mapping the logical block reference to the corresponding physical block. This gives us the best of both worlds: the convenience of file operations with the precision of block-level control.
Implementing core components
Database interface
First, let’s create our main database interface.
Here is the test case that we want to pass. We just want to test that the path we pass in exists and is a directory. Note that we’re using 400
as the block size and 8
as buffer size because Sciore recommends this for learning purposes. Real world database systems use much larger numbers.
Our SimpleDB
struct will provide the entry point for all database interactions.
Managing files
The FileManager
is our bridge to the operating system. It handles three key responsibilities:
- Creating and managing the database directory
- Tracking open files
- Reading and writing blocks of data to the
Page
Here’s the basic structure.
When creating a new FileManager
, we need to:
- set up the database directory
- clean up any temporary files
- initialize open files tracking
Note that we’re also using Mutex
to provide thread-safe access to the open_files
HashMap. The FileManager
might be accessed from multiple threads in the application, so Mutex
ensures that only one thread can access the HashMap at any one time.
Working with Blocks and Pages
To understand how data is stored and retrieved, we need to understand these two concepts:
- BlockId: identifies where data lives on disks (files)
- Page: holds the actual data in memory
Here’s how they work together:
Implementing BlockId
Implementing Page
The Page
will have the following functions:
- buffer (
vec
) to hold the contents of the block - setter functions to convert data into bytes and write it into the buffer
set_int
,set_string
,set_bytes
- and equivalent getter functions to convert bytes into the appropriate data types
get_int
,get_string
,get_bytes
contents
that returns a mutable buffer for writing into
Now that we have our BlockId
and Page
implementations, we have the building blocks to finish the read
and write
functions in our FileManager
.
Reading data
We want read
to:
- get the filename from
BlockId
- figure out the offset from
BlockId
- seek to the correct block position
- read the contents into the page’s buffer
Writing data
And the write
function does something similar, but writes to the file using the page’s buffer.
Testing read and write
Now we can write a test to make sure that read
/write
work as we expect.
With this, we now have a working implementation of a FileManager
that interacts with the OS file system and Pager
which contains the contents of each block of our ‘disk’ (file).
What we’ve built
In this first part, we’ve implemented these fundamental building blocks:
FileManager
: handles disk operations, providing an interface between our database engine and the operating systemBlockId
: maps logical blocks to physical blocksPage
: holds data content in memory
Next
The next chapter will deal with transaction management.
Have some thoughts on this post? Reply with an email.