commit e6ed39f0df316a872bbdc339faf7f9058e14a53b Author: 程广 Date: Sun Nov 9 17:01:59 2025 +0800 feat(gobplustree): 实现B+树存储基础模块 新增BlockFile结构体及其实现,用于支持B+树数据的持久化存储。 该模块提供固定大小页面的文件读写功能,包括页面的创建、打开、关闭、 读取与写入等操作,并在文件容量不足时自动扩展。同时添加了相关单元测试 以确保功能正确性。初始化Go模块配置文件go.mod。 diff --git a/file.go b/file.go new file mode 100644 index 0000000..dde7a1a --- /dev/null +++ b/file.go @@ -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 +} diff --git a/file_test.go b/file_test.go new file mode 100644 index 0000000..e99d3d9 --- /dev/null +++ b/file_test.go @@ -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) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5e81caa --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.kingecg.top/kingecg/gobplustree + +go 1.25.1