feat(gobplustree): 实现B+树存储基础模块
新增BlockFile结构体及其实现,用于支持B+树数据的持久化存储。 该模块提供固定大小页面的文件读写功能,包括页面的创建、打开、关闭、 读取与写入等操作,并在文件容量不足时自动扩展。同时添加了相关单元测试 以确保功能正确性。初始化Go模块配置文件go.mod。
This commit is contained in:
commit
e6ed39f0df
|
|
@ -0,0 +1,130 @@
|
|||
// Package gobplustree implements a B+ tree data structure and related utilities.
|
||||
package gobplustree
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPageIdOutOfRange is returned when trying to access a page that doesn't exist in the file.
|
||||
ErrPageIdOutOfRange = errors.New("page id out of range")
|
||||
)
|
||||
|
||||
// BlockFile represents a file divided into fixed-size blocks or pages.
|
||||
// It provides methods for reading and writing these pages.
|
||||
type BlockFile struct {
|
||||
PageCount int64
|
||||
FileName string
|
||||
file *os.File
|
||||
}
|
||||
|
||||
// Constants defining the structure of the block file.
|
||||
const (
|
||||
// BlockFileHeaderSize is the size of the file header in bytes.
|
||||
BlockFileHeaderSize = 8
|
||||
|
||||
// BlockFilePageSize is the size of each page in bytes.
|
||||
BlockFilePageSize = 4096
|
||||
|
||||
// InitPageCount is the initial number of pages when creating a new file.
|
||||
InitPageCount = 1024
|
||||
)
|
||||
|
||||
// NewBlockFile creates a new BlockFile instance.
|
||||
// fileName: the name of the file to be created or opened.
|
||||
// initialPageCount: the initial number of pages for the file.
|
||||
func NewBlockFile(fileName string, initialPageCount int64) (*BlockFile, error) {
|
||||
return &BlockFile{
|
||||
PageCount: initialPageCount,
|
||||
FileName: fileName,
|
||||
file: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Open opens the file and initializes it if it's empty.
|
||||
// Returns an error if the file cannot be opened or initialized.
|
||||
func (bf *BlockFile) Open() error {
|
||||
file, err := os.OpenFile(bf.FileName, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bf.file = file
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If file is empty, initialize it with default pages
|
||||
if fileInfo.Size() == 0 {
|
||||
// bf.PageCount = InitPageCount
|
||||
_, err := bf.file.Write(make([]byte, BlockFileHeaderSize))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = bf.file.Write(make([]byte, BlockFilePageSize*InitPageCount))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
bf.PageCount = fileInfo.Size() / BlockFilePageSize
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the underlying file.
|
||||
// Returns an error if the file cannot be closed.
|
||||
func (bf *BlockFile) Close() error {
|
||||
return bf.file.Close()
|
||||
}
|
||||
|
||||
// Read reads a page from the file by its page ID.
|
||||
// pageId: the ID of the page to read.
|
||||
// Returns the page data and an error if the operation fails.
|
||||
func (bf *BlockFile) Read(pageId int64) ([]byte, error) {
|
||||
if pageId >= bf.PageCount || pageId < 0 {
|
||||
return nil, ErrPageIdOutOfRange
|
||||
}
|
||||
|
||||
data := make([]byte, BlockFilePageSize)
|
||||
n, err := bf.file.ReadAt(data, pageId*BlockFilePageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if we read the full page
|
||||
if n != BlockFilePageSize {
|
||||
return nil, ErrPageIdOutOfRange
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// enlarge increases the file size by adding more pages.
|
||||
// count: the number of pages to add.
|
||||
// Returns an error if the operation fails.
|
||||
func (bf *BlockFile) enlarge(count int64) error {
|
||||
_, err := bf.file.WriteAt(make([]byte, BlockFilePageSize*count), bf.PageCount*BlockFilePageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bf.PageCount += count
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes data to a specific page in the file.
|
||||
// pageId: the ID of the page to write to.
|
||||
// data: the data to write.
|
||||
// Returns an error if the operation fails.
|
||||
func (bf *BlockFile) Write(pageId int64, data []byte) error {
|
||||
if pageId >= bf.PageCount {
|
||||
err := bf.enlarge(pageId - bf.PageCount + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := bf.file.WriteAt(data, pageId*BlockFilePageSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package gobplustree
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestNewBlockFile tests the creation of a new BlockFile instance.
|
||||
func TestNewBlockFile(t *testing.T) {
|
||||
bf, err := NewBlockFile("test.db", 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BlockFile: %v", err)
|
||||
}
|
||||
|
||||
if bf.FileName != "test.db" {
|
||||
t.Errorf("Expected filename 'test.db', got '%s'", bf.FileName)
|
||||
}
|
||||
|
||||
if bf.PageCount != 100 {
|
||||
t.Errorf("Expected PageCount 100, got %d", bf.PageCount)
|
||||
}
|
||||
|
||||
if bf.file != nil {
|
||||
t.Errorf("Expected file to be nil, got %v", bf.file)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlockFileOpenAndClose tests opening and closing a BlockFile.
|
||||
func TestBlockFileOpenAndClose(t *testing.T) {
|
||||
filename := "test_open.db"
|
||||
bf, err := NewBlockFile(filename, InitPageCount)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Test opening the file
|
||||
err = bf.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Check that file was created
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
t.Error("File was not created")
|
||||
}
|
||||
|
||||
// Test closing the file
|
||||
err = bf.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to close BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
os.Remove(filename)
|
||||
}
|
||||
|
||||
// TestBlockFileReadWrite tests reading and writing pages to a BlockFile.
|
||||
func TestBlockFileReadWrite(t *testing.T) {
|
||||
filename := "test_rw.db"
|
||||
bf, err := NewBlockFile(filename, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BlockFile: %v", err)
|
||||
}
|
||||
|
||||
err = bf.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Test writing data
|
||||
testData := []byte("Hello, BlockFile!")
|
||||
err = bf.Write(0, testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Pad testData to full page size for comparison
|
||||
paddedTestData := make([]byte, BlockFilePageSize)
|
||||
copy(paddedTestData, testData)
|
||||
|
||||
// Test reading data
|
||||
data, err := bf.Read(0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read from BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Check that we got the data we wrote
|
||||
if !bytes.Equal(data, paddedTestData) {
|
||||
t.Errorf("Read data does not match written data. Expected: %s, Got: %s",
|
||||
string(testData), string(data[:len(testData)]))
|
||||
}
|
||||
|
||||
// Test reading a non-existent page
|
||||
_, err = bf.Read(100)
|
||||
if err != ErrPageIdOutOfRange {
|
||||
t.Errorf("Expected ErrPageIdOutOfRange, got %v", err)
|
||||
}
|
||||
|
||||
// Test writing to a page that requires enlarging the file
|
||||
longData := make([]byte, 100)
|
||||
for i := range longData {
|
||||
longData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
err = bf.Write(15, longData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to page requiring file enlargement: %v", err)
|
||||
}
|
||||
|
||||
// Read the data back
|
||||
readData, err := bf.Read(15)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read enlarged page: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(readData[:len(longData)], longData) {
|
||||
t.Error("Read data from enlarged page does not match written data")
|
||||
}
|
||||
|
||||
err = bf.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to close BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
os.Remove(filename)
|
||||
}
|
||||
|
||||
// TestBlockFileEnlarge tests the internal enlarge method.
|
||||
func TestBlockFileEnlarge(t *testing.T) {
|
||||
filename := "test_enlarge.db"
|
||||
bf, err := NewBlockFile(filename, 5)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create BlockFile: %v", err)
|
||||
}
|
||||
|
||||
err = bf.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open BlockFile: %v", err)
|
||||
}
|
||||
|
||||
initialPageCount := bf.PageCount
|
||||
|
||||
// Manually call enlarge
|
||||
err = bf.enlarge(10)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to enlarge BlockFile: %v", err)
|
||||
}
|
||||
|
||||
if bf.PageCount != initialPageCount+10 {
|
||||
t.Errorf("Expected PageCount to be %d, got %d", initialPageCount+10, bf.PageCount)
|
||||
}
|
||||
|
||||
err = bf.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to close BlockFile: %v", err)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
os.Remove(filename)
|
||||
}
|
||||
Loading…
Reference in New Issue