添加 Clean 和 Destroy 功能

主要改动:
- Engine: 添加 Clean() 和 Destroy() 方法
- Table: 添加 Clean() 和 Destroy() 方法(不持有 Database 引用)
- Database: 添加 Clean()、CleanTable()、DestroyTable()、Destroy() 方法
- 自动 flush: 添加长时间无写入自动 flush 策略(默认 30 秒)
- WebUI 优化: 优化分页查询性能

新增功能:
- Clean(): 清除数据但保留结构,Engine/Table/Database 仍可用
- Destroy(): 销毁并删除所有文件,对象不可用
- CleanTable(name): 清除指定表的数据
- DestroyTable(name): 销毁指定表并从 Database 中删除
- 自动 flush 监控: 后台定期检查,空闲时自动持久化

代码优化:
- Engine.Close(): 支持 Destroy 后调用,不会 panic
- 二级索引持久化: 在 flush 时自动持久化索引
- WebUI 分页: 预构建字段类型 map,减少 Schema 查询
- 职责分离: Table 不再持有 Database 引用

测试覆盖:
- engine_clean_test.go: Engine Clean/Destroy 测试
- table_clean_test.go: Table Clean/Destroy 测试
- database_clean_test.go: Database Clean/Destroy 测试
- database_table_ops_test.go: Database CleanTable/DestroyTable 测试
This commit is contained in:
2025-10-09 01:03:22 +08:00
parent 23843493b8
commit 9175b98202
8 changed files with 1413 additions and 123 deletions

View File

@@ -254,3 +254,86 @@ func (db *Database) GetAllTablesInfo() map[string]*Table {
maps.Copy(result, db.tables) maps.Copy(result, db.tables)
return result return result
} }
// CleanTable 清除指定表的数据(保留表结构)
func (db *Database) CleanTable(name string) error {
db.mu.RLock()
table, exists := db.tables[name]
db.mu.RUnlock()
if !exists {
return fmt.Errorf("table %s does not exist", name)
}
return table.Clean()
}
// DestroyTable 销毁指定表并从 Database 中删除
func (db *Database) DestroyTable(name string) error {
db.mu.Lock()
defer db.mu.Unlock()
table, exists := db.tables[name]
if !exists {
return fmt.Errorf("table %s does not exist", name)
}
// 1. 销毁表(删除文件)
if err := table.Destroy(); err != nil {
return fmt.Errorf("destroy table: %w", err)
}
// 2. 从内存中删除
delete(db.tables, name)
// 3. 从元数据中删除
newTables := make([]TableInfo, 0, len(db.metadata.Tables)-1)
for _, info := range db.metadata.Tables {
if info.Name != name {
newTables = append(newTables, info)
}
}
db.metadata.Tables = newTables
// 4. 保存元数据
return db.saveMetadata()
}
// Clean 清除所有表的数据(保留表结构和 Database 可用)
func (db *Database) Clean() error {
db.mu.Lock()
defer db.mu.Unlock()
// 清除所有表的数据
for name, table := range db.tables {
if err := table.Clean(); err != nil {
return fmt.Errorf("clean table %s: %w", name, err)
}
}
return nil
}
// Destroy 销毁整个数据库并删除所有数据文件
func (db *Database) Destroy() error {
db.mu.Lock()
defer db.mu.Unlock()
// 1. 关闭所有表
for _, table := range db.tables {
if err := table.Close(); err != nil {
return fmt.Errorf("close table: %w", err)
}
}
// 2. 删除整个数据库目录
if err := os.RemoveAll(db.dir); err != nil {
return fmt.Errorf("remove database directory: %w", err)
}
// 3. 清空内存中的表
db.tables = make(map[string]*Table)
db.metadata.Tables = nil
return nil
}

296
database_clean_test.go Normal file
View File

@@ -0,0 +1,296 @@
package srdb
import (
"fmt"
"os"
"testing"
)
func TestDatabaseClean(t *testing.T) {
dir := "./test_db_clean_data"
defer os.RemoveAll(dir)
// 1. 创建数据库
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
// 2. 创建多个表并插入数据
// 表 1: users
usersSchema := NewSchema("users", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "User ID"},
{Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"},
})
usersTable, err := db.CreateTable("users", usersSchema)
if err != nil {
t.Fatal(err)
}
for i := 0; i < 50; i++ {
usersTable.Insert(map[string]any{
"id": int64(i),
"name": "user" + string(rune(i)),
})
}
// 表 2: orders
ordersSchema := NewSchema("orders", []Field{
{Name: "order_id", Type: FieldTypeInt64, Indexed: true, Comment: "Order ID"},
{Name: "amount", Type: FieldTypeInt64, Indexed: false, Comment: "Amount"},
})
ordersTable, err := db.CreateTable("orders", ordersSchema)
if err != nil {
t.Fatal(err)
}
for i := 0; i < 30; i++ {
ordersTable.Insert(map[string]any{
"order_id": int64(i),
"amount": int64(i * 100),
})
}
// 3. 验证数据存在
usersStats := usersTable.Stats()
ordersStats := ordersTable.Stats()
t.Logf("Before Clean - Users: %d rows, Orders: %d rows",
usersStats.TotalRows, ordersStats.TotalRows)
if usersStats.TotalRows == 0 || ordersStats.TotalRows == 0 {
t.Error("Expected data in tables")
}
// 4. 清除所有表的数据
err = db.Clean()
if err != nil {
t.Fatal(err)
}
// 5. 验证数据已清除
usersStats = usersTable.Stats()
ordersStats = ordersTable.Stats()
t.Logf("After Clean - Users: %d rows, Orders: %d rows",
usersStats.TotalRows, ordersStats.TotalRows)
if usersStats.TotalRows != 0 {
t.Errorf("Expected 0 rows in users, got %d", usersStats.TotalRows)
}
if ordersStats.TotalRows != 0 {
t.Errorf("Expected 0 rows in orders, got %d", ordersStats.TotalRows)
}
// 6. 验证表结构仍然存在
tables := db.ListTables()
if len(tables) != 2 {
t.Errorf("Expected 2 tables, got %d", len(tables))
}
// 7. 验证可以继续插入数据
err = usersTable.Insert(map[string]any{
"id": int64(100),
"name": "new_user",
})
if err != nil {
t.Fatal(err)
}
usersStats = usersTable.Stats()
if usersStats.TotalRows != 1 {
t.Errorf("Expected 1 row after insert, got %d", usersStats.TotalRows)
}
db.Close()
}
func TestDatabaseDestroy(t *testing.T) {
dir := "./test_db_destroy_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
schema := NewSchema("test", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
})
table, err := db.CreateTable("test", schema)
if err != nil {
t.Fatal(err)
}
// 插入数据
for i := 0; i < 20; i++ {
table.Insert(map[string]any{"id": int64(i)})
}
// 2. 验证数据存在
stats := table.Stats()
t.Logf("Before Destroy: %d rows", stats.TotalRows)
if stats.TotalRows == 0 {
t.Error("Expected data in table")
}
// 3. 销毁数据库
err = db.Destroy()
if err != nil {
t.Fatal(err)
}
// 4. 验证数据目录已删除
if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Error("Database directory should be deleted")
}
// 5. 验证数据库不可用
tables := db.ListTables()
if len(tables) != 0 {
t.Errorf("Expected 0 tables after destroy, got %d", len(tables))
}
}
func TestDatabaseCleanMultipleTables(t *testing.T) {
dir := "./test_db_clean_multi_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和多个表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
// 创建 5 个表
for i := 0; i < 5; i++ {
tableName := fmt.Sprintf("table%d", i)
schema := NewSchema(tableName, []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
{Name: "value", Type: FieldTypeString, Indexed: false, Comment: "Value"},
})
table, err := db.CreateTable(tableName, schema)
if err != nil {
t.Fatal(err)
}
// 每个表插入 10 条数据
for j := 0; j < 10; j++ {
table.Insert(map[string]any{
"id": int64(j),
"value": fmt.Sprintf("value_%d_%d", i, j),
})
}
}
// 2. 验证所有表都有数据
tables := db.ListTables()
if len(tables) != 5 {
t.Fatalf("Expected 5 tables, got %d", len(tables))
}
totalRows := 0
for _, tableName := range tables {
table, _ := db.GetTable(tableName)
stats := table.Stats()
totalRows += int(stats.TotalRows)
}
t.Logf("Total rows before clean: %d", totalRows)
if totalRows == 0 {
t.Error("Expected data in tables")
}
// 3. 清除所有表
err = db.Clean()
if err != nil {
t.Fatal(err)
}
// 4. 验证所有表数据已清除
totalRows = 0
for _, tableName := range tables {
table, _ := db.GetTable(tableName)
stats := table.Stats()
totalRows += int(stats.TotalRows)
if stats.TotalRows != 0 {
t.Errorf("Table %s should have 0 rows, got %d", tableName, stats.TotalRows)
}
}
t.Logf("Total rows after clean: %d", totalRows)
// 5. 验证表结构仍然存在
tables = db.ListTables()
if len(tables) != 5 {
t.Errorf("Expected 5 tables after clean, got %d", len(tables))
}
}
func TestDatabaseCleanAndReopen(t *testing.T) {
dir := "./test_db_clean_reopen_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
schema := NewSchema("test", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
})
table, err := db.CreateTable("test", schema)
if err != nil {
t.Fatal(err)
}
// 插入数据
for i := 0; i < 50; i++ {
table.Insert(map[string]any{"id": int64(i)})
}
// 2. 清除数据
err = db.Clean()
if err != nil {
t.Fatal(err)
}
// 3. 关闭并重新打开
db.Close()
db2, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db2.Close()
// 4. 验证表存在但数据为空
tables := db2.ListTables()
if len(tables) != 1 {
t.Errorf("Expected 1 table, got %d", len(tables))
}
table2, err := db2.GetTable("test")
if err != nil {
t.Fatal(err)
}
stats := table2.Stats()
if stats.TotalRows != 0 {
t.Errorf("Expected 0 rows after reopen, got %d", stats.TotalRows)
}
// 5. 验证可以插入新数据
err = table2.Insert(map[string]any{"id": int64(100)})
if err != nil {
t.Fatal(err)
}
stats = table2.Stats()
if stats.TotalRows != 1 {
t.Errorf("Expected 1 row, got %d", stats.TotalRows)
}
}

195
database_table_ops_test.go Normal file
View File

@@ -0,0 +1,195 @@
package srdb
import (
"os"
"testing"
)
func TestDatabaseCleanTable(t *testing.T) {
dir := "./test_db_clean_table_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := NewSchema("users", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
{Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"},
})
table, err := db.CreateTable("users", schema)
if err != nil {
t.Fatal(err)
}
// 2. 插入数据
for i := 0; i < 50; i++ {
table.Insert(map[string]any{
"id": int64(i),
"name": "user",
})
}
// 3. 验证数据存在
stats := table.Stats()
if stats.TotalRows == 0 {
t.Error("Expected data in table")
}
// 4. 清除表数据
err = db.CleanTable("users")
if err != nil {
t.Fatal(err)
}
// 5. 验证数据已清除
stats = table.Stats()
if stats.TotalRows != 0 {
t.Errorf("Expected 0 rows after clean, got %d", stats.TotalRows)
}
// 6. 验证表仍然存在
tables := db.ListTables()
found := false
for _, name := range tables {
if name == "users" {
found = true
break
}
}
if !found {
t.Error("Table should still exist after clean")
}
// 7. 验证可以继续插入
err = table.Insert(map[string]any{
"id": int64(100),
"name": "new_user",
})
if err != nil {
t.Fatal(err)
}
stats = table.Stats()
if stats.TotalRows != 1 {
t.Errorf("Expected 1 row, got %d", stats.TotalRows)
}
}
func TestDatabaseDestroyTable(t *testing.T) {
dir := "./test_db_destroy_table_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := NewSchema("test", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
})
table, err := db.CreateTable("test", schema)
if err != nil {
t.Fatal(err)
}
// 2. 插入数据
for i := 0; i < 30; i++ {
table.Insert(map[string]any{"id": int64(i)})
}
// 3. 验证数据存在
stats := table.Stats()
if stats.TotalRows == 0 {
t.Error("Expected data in table")
}
// 4. 销毁表
err = db.DestroyTable("test")
if err != nil {
t.Fatal(err)
}
// 5. 验证表已从 Database 中删除
tables := db.ListTables()
for _, name := range tables {
if name == "test" {
t.Error("Table should be removed from database")
}
}
// 6. 验证无法再获取该表
_, err = db.GetTable("test")
if err == nil {
t.Error("Should not be able to get table after destroy")
}
}
func TestDatabaseDestroyTableMultiple(t *testing.T) {
dir := "./test_db_destroy_multi_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和多个表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := NewSchema("test", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
})
// 创建 3 个表
for i := 1; i <= 3; i++ {
tableName := "table" + string(rune('0'+i))
_, err := db.CreateTable(tableName, schema)
if err != nil {
t.Fatal(err)
}
}
// 2. 验证有 3 个表
tables := db.ListTables()
if len(tables) != 3 {
t.Fatalf("Expected 3 tables, got %d", len(tables))
}
// 3. 销毁中间的表
err = db.DestroyTable("table2")
if err != nil {
t.Fatal(err)
}
// 4. 验证只剩 2 个表
tables = db.ListTables()
if len(tables) != 2 {
t.Errorf("Expected 2 tables, got %d", len(tables))
}
// 5. 验证剩余的表是正确的
hasTable1 := false
hasTable3 := false
for _, name := range tables {
if name == "table1" {
hasTable1 = true
}
if name == "table3" {
hasTable3 = true
}
if name == "table2" {
t.Error("table2 should be destroyed")
}
}
if !hasTable1 || !hasTable3 {
t.Error("table1 and table3 should still exist")
}
}

288
engine.go
View File

@@ -13,6 +13,7 @@ import (
const ( const (
DefaultMemTableSize = 64 * 1024 * 1024 // 64 MB DefaultMemTableSize = 64 * 1024 * 1024 // 64 MB
DefaultAutoFlushTimeout = 30 * time.Second // 30 秒无写入自动 flush
) )
// Engine 存储引擎 // Engine 存储引擎
@@ -26,8 +27,12 @@ type Engine struct {
versionSet *VersionSet // MANIFEST 管理器 versionSet *VersionSet // MANIFEST 管理器
compactionManager *CompactionManager // Compaction 管理器 compactionManager *CompactionManager // Compaction 管理器
seq atomic.Int64 seq atomic.Int64
mu sync.RWMutex
flushMu sync.Mutex flushMu sync.Mutex
// 自动 flush 相关
autoFlushTimeout time.Duration
lastWriteTime atomic.Int64 // 最后写入时间UnixNano
stopAutoFlush chan struct{}
} }
// EngineOptions 配置选项 // EngineOptions 配置选项
@@ -35,6 +40,7 @@ type EngineOptions struct {
Dir string Dir string
MemTableSize int64 MemTableSize int64
Schema *Schema // 可选的 Schema 定义 Schema *Schema // 可选的 Schema 定义
AutoFlushTimeout time.Duration // 自动 flush 超时时间0 表示禁用
} }
// OpenEngine 打开数据库 // OpenEngine 打开数据库
@@ -183,6 +189,18 @@ func OpenEngine(opts *EngineOptions) (*Engine, error) {
engine.verifyAndRepairIndexes() engine.verifyAndRepairIndexes()
} }
// 设置自动 flush 超时时间
if opts.AutoFlushTimeout > 0 {
engine.autoFlushTimeout = opts.AutoFlushTimeout
} else {
engine.autoFlushTimeout = DefaultAutoFlushTimeout
}
engine.stopAutoFlush = make(chan struct{})
engine.lastWriteTime.Store(time.Now().UnixNano())
// 启动自动 flush 监控
go engine.autoFlushMonitor()
return engine, nil return engine, nil
} }
@@ -230,7 +248,10 @@ func (e *Engine) Insert(data map[string]any) error {
e.indexManager.AddToIndexes(data, seq) e.indexManager.AddToIndexes(data, seq)
} }
// 7. 检查是否需要切换 MemTable // 7. 更新最后写入时间
e.lastWriteTime.Store(time.Now().UnixNano())
// 8. 检查是否需要切换 MemTable
if e.memtableManager.ShouldSwitch() { if e.memtableManager.ShouldSwitch() {
go e.switchMemTable() go e.switchMemTable()
} }
@@ -372,7 +393,12 @@ func (e *Engine) flushImmutable(imm *ImmutableMemTable, walNumber int64) error {
// 7. 从 Immutable 列表中移除 // 7. 从 Immutable 列表中移除
e.memtableManager.RemoveImmutable(imm) e.memtableManager.RemoveImmutable(imm)
// 8. Compaction 由后台线程负责,不在 flush 路径中触发 // 8. 持久化索引(防止崩溃丢失索引数据)
if e.indexManager != nil {
e.indexManager.BuildAll()
}
// 9. Compaction 由后台线程负责,不在 flush 路径中触发
// 避免同步 compaction 导致刚创建的文件立即被删除 // 避免同步 compaction 导致刚创建的文件立即被删除
// e.compactionManager.MaybeCompact() // e.compactionManager.MaybeCompact()
@@ -436,37 +462,90 @@ func (e *Engine) recover() error {
return nil return nil
} }
// autoFlushMonitor 自动 flush 监控
func (e *Engine) autoFlushMonitor() {
ticker := time.NewTicker(e.autoFlushTimeout / 2) // 每半个超时时间检查一次
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 检查是否超时
lastWrite := time.Unix(0, e.lastWriteTime.Load())
if time.Since(lastWrite) >= e.autoFlushTimeout {
// 检查 MemTable 是否有数据
active := e.memtableManager.GetActive()
if active != nil && active.Size() > 0 {
// 触发 flush
e.Flush()
}
}
case <-e.stopAutoFlush:
return
}
}
}
// Flush 手动刷新 Active MemTable 到磁盘
func (e *Engine) Flush() error {
// 检查 Active MemTable 是否有数据
active := e.memtableManager.GetActive()
if active == nil || active.Size() == 0 {
return nil // 没有数据,无需 flush
}
// 强制切换 MemTableswitchMemTable 内部有锁)
return e.switchMemTable()
}
// Close 关闭引擎 // Close 关闭引擎
func (e *Engine) Close() error { func (e *Engine) Close() error {
// 1. 停止后台 Compaction // 1. 停止自动 flush 监控(如果还在运行)
if e.stopAutoFlush != nil {
select {
case <-e.stopAutoFlush:
// 已经关闭,跳过
default:
close(e.stopAutoFlush)
}
}
// 2. 停止 Compaction Manager
if e.compactionManager != nil { if e.compactionManager != nil {
e.compactionManager.Stop() e.compactionManager.Stop()
} }
// 2. Flush Active MemTable // 3. 刷新 Active MemTable(确保所有数据都写入磁盘)
if e.memtableManager.GetActiveCount() > 0 { // 检查 memtableManager 是否存在(可能已被 Destroy
// 切换并 Flush if e.memtableManager != nil {
e.switchMemTable() e.Flush()
} }
// 等待所有 Immutable Flush 完成 // 3. 关闭 WAL Manager
if e.walManager != nil {
e.walManager.Close()
}
// 4. 等待所有 Immutable Flush 完成
// TODO: 添加更优雅的等待机制 // TODO: 添加更优雅的等待机制
if e.memtableManager != nil {
for e.memtableManager.GetImmutableCount() > 0 { for e.memtableManager.GetImmutableCount() > 0 {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
}
// 3. 保存所有索引 // 5. 保存所有索引
if e.indexManager != nil { if e.indexManager != nil {
e.indexManager.BuildAll() e.indexManager.BuildAll()
e.indexManager.Close() e.indexManager.Close()
} }
// 4. 关闭 VersionSet // 6. 关闭 VersionSet
if e.versionSet != nil { if e.versionSet != nil {
e.versionSet.Close() e.versionSet.Close()
} }
// 5. 关闭 WAL Manager // 7. 关闭 WAL Manager
if e.walManager != nil { if e.walManager != nil {
e.walManager.Close() e.walManager.Close()
} }
@@ -479,6 +558,133 @@ func (e *Engine) Close() error {
return nil return nil
} }
// Clean 清除所有数据(保留 Engine 可用)
func (e *Engine) Clean() error {
e.flushMu.Lock()
defer e.flushMu.Unlock()
// 0. 停止自动 flush 监控(临时)
if e.stopAutoFlush != nil {
close(e.stopAutoFlush)
}
// 1. 停止 Compaction Manager
if e.compactionManager != nil {
e.compactionManager.Stop()
}
// 2. 等待所有 Immutable Flush 完成
for e.memtableManager.GetImmutableCount() > 0 {
time.Sleep(100 * time.Millisecond)
}
// 3. 清空 MemTable
e.memtableManager = NewMemTableManager(DefaultMemTableSize)
// 2. 删除所有 WAL 文件
if e.walManager != nil {
e.walManager.Close()
walDir := filepath.Join(e.dir, "wal")
os.RemoveAll(walDir)
os.MkdirAll(walDir, 0755)
// 重新创建 WAL Manager
walMgr, err := NewWALManager(walDir)
if err != nil {
return fmt.Errorf("recreate wal manager: %w", err)
}
e.walManager = walMgr
e.memtableManager.SetActiveWAL(walMgr.GetCurrentNumber())
}
// 3. 删除所有 SST 文件
if e.sstManager != nil {
e.sstManager.Close()
sstDir := filepath.Join(e.dir, "sst")
os.RemoveAll(sstDir)
os.MkdirAll(sstDir, 0755)
// 重新创建 SST Manager
sstMgr, err := NewSSTableManager(sstDir)
if err != nil {
return fmt.Errorf("recreate sst manager: %w", err)
}
e.sstManager = sstMgr
}
// 4. 删除所有索引文件
if e.indexManager != nil {
e.indexManager.Close()
indexFiles, _ := filepath.Glob(filepath.Join(e.dir, "idx_*.sst"))
for _, f := range indexFiles {
os.Remove(f)
}
// 重新创建 Index Manager
if e.schema != nil {
e.indexManager = NewIndexManager(e.dir, e.schema)
}
}
// 5. 重置 MANIFEST
if e.versionSet != nil {
e.versionSet.Close()
manifestDir := e.dir
os.Remove(filepath.Join(manifestDir, "MANIFEST"))
os.Remove(filepath.Join(manifestDir, "CURRENT"))
// 重新创建 VersionSet
versionSet, err := NewVersionSet(manifestDir)
if err != nil {
return fmt.Errorf("recreate version set: %w", err)
}
e.versionSet = versionSet
}
// 6. 重新创建 Compaction Manager
sstDir := filepath.Join(e.dir, "sst")
e.compactionManager = NewCompactionManager(sstDir, e.versionSet, e.sstManager)
if e.schema != nil {
e.compactionManager.SetSchema(e.schema)
}
e.compactionManager.Start()
// 7. 重置序列号
e.seq.Store(0)
// 8. 更新最后写入时间
e.lastWriteTime.Store(time.Now().UnixNano())
// 9. 重启自动 flush 监控
e.stopAutoFlush = make(chan struct{})
go e.autoFlushMonitor()
return nil
}
// Destroy 销毁 Engine 并删除所有数据文件
func (e *Engine) Destroy() error {
// 1. 先关闭 Engine
if err := e.Close(); err != nil {
return fmt.Errorf("close engine: %w", err)
}
// 2. 删除整个数据目录
if err := os.RemoveAll(e.dir); err != nil {
return fmt.Errorf("remove data directory: %w", err)
}
// 3. 标记 Engine 为不可用(将所有管理器设为 nil
e.walManager = nil
e.sstManager = nil
e.memtableManager = nil
e.versionSet = nil
e.compactionManager = nil
e.indexManager = nil
return nil
}
// TableStats 统计信息 // TableStats 统计信息
type TableStats struct { type TableStats struct {
MemTableSize int64 MemTableSize int64
@@ -585,64 +791,6 @@ func (e *Engine) Query() *QueryBuilder {
return newQueryBuilder(e) return newQueryBuilder(e)
} }
// scanAllWithBuilder 使用 QueryBuilder 全表扫描
func (e *Engine) scanAllWithBuilder(qb *QueryBuilder) ([]*SSTableRow, error) {
// 使用 map 去重(同一个 seq 只保留一次)
rowMap := make(map[int64]*SSTableRow)
// 扫描 Active MemTable
iter := e.memtableManager.NewIterator()
for iter.Next() {
seq := iter.Key()
row, err := e.Get(seq)
if err == nil && qb.Match(row.Data) {
rowMap[seq] = row
}
}
// 扫描 Immutable MemTables
immutables := e.memtableManager.GetImmutables()
for _, imm := range immutables {
iter := imm.NewIterator()
for iter.Next() {
seq := iter.Key()
if _, exists := rowMap[seq]; !exists {
row, err := e.Get(seq)
if err == nil && qb.Match(row.Data) {
rowMap[seq] = row
}
}
}
}
// 扫描 SST 文件
readers := e.sstManager.GetReaders()
for _, reader := range readers {
header := reader.GetHeader()
for seq := header.MinKey; seq <= header.MaxKey; seq++ {
if _, exists := rowMap[seq]; !exists {
row, err := reader.Get(seq)
if err == nil && qb.Match(row.Data) {
rowMap[seq] = row
}
}
}
}
// 转换为数组并按 Seq 排序
results := make([]*SSTableRow, 0, len(rowMap))
for _, row := range rowMap {
results = append(results, row)
}
// 按 Seq 排序(保证查询结果有序)
sort.Slice(results, func(i, j int) bool {
return results[i].Seq < results[j].Seq
})
return results, nil
}
// verifyAndRepairIndexes 验证并修复索引 // verifyAndRepairIndexes 验证并修复索引
func (e *Engine) verifyAndRepairIndexes() error { func (e *Engine) verifyAndRepairIndexes() error {
if e.indexManager == nil { if e.indexManager == nil {

244
engine_clean_test.go Normal file
View File

@@ -0,0 +1,244 @@
package srdb
import (
"os"
"testing"
"time"
)
func TestEngineClean(t *testing.T) {
dir := "./test_clean_data"
defer os.RemoveAll(dir)
// 1. 创建 Engine 并插入数据
engine, err := OpenEngine(&EngineOptions{
Dir: dir,
})
if err != nil {
t.Fatal(err)
}
// 插入一些数据
for i := 0; i < 100; i++ {
err := engine.Insert(map[string]any{
"id": i,
"name": "test",
})
if err != nil {
t.Fatal(err)
}
}
// 强制 flush
engine.Flush()
time.Sleep(500 * time.Millisecond)
// 验证数据存在
stats := engine.Stats()
t.Logf("Before Clean: MemTable=%d, SST=%d, Total=%d",
stats.MemTableCount, stats.SSTCount, stats.TotalRows)
if stats.TotalRows == 0 {
t.Errorf("Expected some rows, got 0")
}
// 2. 清除数据
err = engine.Clean()
if err != nil {
t.Fatal(err)
}
// 3. 验证数据已清除
stats = engine.Stats()
t.Logf("After Clean: MemTable=%d, SST=%d, Total=%d",
stats.MemTableCount, stats.SSTCount, stats.TotalRows)
if stats.TotalRows != 0 {
t.Errorf("Expected 0 rows after clean, got %d", stats.TotalRows)
}
// 4. 验证 Engine 仍然可用
err = engine.Insert(map[string]any{
"id": 1,
"name": "after_clean",
})
if err != nil {
t.Fatal(err)
}
stats = engine.Stats()
if stats.TotalRows != 1 {
t.Errorf("Expected 1 row after insert, got %d", stats.TotalRows)
}
engine.Close()
}
func TestEngineDestroy(t *testing.T) {
dir := "./test_destroy_data"
defer os.RemoveAll(dir)
// 1. 创建 Engine 并插入数据
engine, err := OpenEngine(&EngineOptions{
Dir: dir,
})
if err != nil {
t.Fatal(err)
}
// 插入一些数据
for i := 0; i < 50; i++ {
err := engine.Insert(map[string]any{
"id": i,
"name": "test",
})
if err != nil {
t.Fatal(err)
}
}
// 验证数据存在
stats := engine.Stats()
t.Logf("Before Destroy: MemTable=%d, SST=%d, Total=%d",
stats.MemTableCount, stats.SSTCount, stats.TotalRows)
// 2. 销毁 Engine
err = engine.Destroy()
if err != nil {
t.Fatal(err)
}
// 3. 验证数据目录已删除
if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Errorf("Data directory should be deleted")
}
// 4. 验证 Engine 不可用(尝试插入会失败)
err = engine.Insert(map[string]any{
"id": 1,
"name": "after_destroy",
})
if err == nil {
t.Errorf("Insert should fail after destroy")
}
}
func TestEngineCleanWithSchema(t *testing.T) {
dir := "./test_clean_schema_data"
defer os.RemoveAll(dir)
// 定义 Schema
schema := NewSchema("test", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "ID"},
{Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"},
})
// 1. 创建 Engine 并插入数据
engine, err := OpenEngine(&EngineOptions{
Dir: dir,
Schema: schema,
})
if err != nil {
t.Fatal(err)
}
// 创建索引
err = engine.CreateIndex("id")
if err != nil {
t.Fatal(err)
}
// 插入数据
for i := 0; i < 50; i++ {
err := engine.Insert(map[string]any{
"id": int64(i),
"name": "test",
})
if err != nil {
t.Fatal(err)
}
}
// 验证索引存在
indexes := engine.ListIndexes()
if len(indexes) != 1 {
t.Errorf("Expected 1 index, got %d", len(indexes))
}
// 2. 清除数据
err = engine.Clean()
if err != nil {
t.Fatal(err)
}
// 3. 验证数据已清除但 Schema 和索引结构保留
stats := engine.Stats()
if stats.TotalRows != 0 {
t.Errorf("Expected 0 rows after clean, got %d", stats.TotalRows)
}
// 验证可以继续插入Schema 仍然有效)
err = engine.Insert(map[string]any{
"id": int64(100),
"name": "after_clean",
})
if err != nil {
t.Fatal(err)
}
engine.Close()
}
func TestEngineCleanAndReopen(t *testing.T) {
dir := "./test_clean_reopen_data"
defer os.RemoveAll(dir)
// 1. 创建 Engine 并插入数据
engine, err := OpenEngine(&EngineOptions{
Dir: dir,
})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 100; i++ {
engine.Insert(map[string]any{
"id": i,
"name": "test",
})
}
// 2. 清除数据
engine.Clean()
// 3. 关闭并重新打开
engine.Close()
engine2, err := OpenEngine(&EngineOptions{
Dir: dir,
})
if err != nil {
t.Fatal(err)
}
defer engine2.Close()
// 4. 验证数据为空
stats := engine2.Stats()
if stats.TotalRows != 0 {
t.Errorf("Expected 0 rows after reopen, got %d", stats.TotalRows)
}
// 5. 验证可以插入新数据
err = engine2.Insert(map[string]any{
"id": 1,
"name": "new_data",
})
if err != nil {
t.Fatal(err)
}
stats = engine2.Stats()
if stats.TotalRows != 1 {
t.Errorf("Expected 1 row, got %d", stats.TotalRows)
}
}

View File

@@ -12,7 +12,6 @@ type Table struct {
dir string // 表目录 dir string // 表目录
schema *Schema // Schema schema *Schema // Schema
engine *Engine // Engine 实例 engine *Engine // Engine 实例
database *Database // 所属数据库
createdAt int64 // 创建时间 createdAt int64 // 创建时间
} }
@@ -42,7 +41,6 @@ func createTable(name string, schema *Schema, db *Database) (*Table, error) {
dir: tableDir, dir: tableDir,
schema: schema, schema: schema,
engine: engine, engine: engine,
database: db,
createdAt: time.Now().Unix(), createdAt: time.Now().Unix(),
} }
@@ -71,7 +69,6 @@ func openTable(name string, db *Database) (*Table, error) {
dir: tableDir, dir: tableDir,
schema: sch, schema: sch,
engine: eng, engine: eng,
database: db,
} }
return table, nil return table, nil
@@ -139,3 +136,19 @@ func (t *Table) Close() error {
func (t *Table) GetCreatedAt() int64 { func (t *Table) GetCreatedAt() int64 {
return t.createdAt return t.createdAt
} }
// Clean 清除表的所有数据(保留表结构和 Table 可用)
func (t *Table) Clean() error {
if t.engine != nil {
return t.engine.Clean()
}
return nil
}
// Destroy 销毁表并删除所有数据文件(不从 Database 中删除)
func (t *Table) Destroy() error {
if t.engine != nil {
return t.engine.Destroy()
}
return nil
}

314
table_clean_test.go Normal file
View File

@@ -0,0 +1,314 @@
package srdb
import (
"os"
"testing"
)
func TestTableClean(t *testing.T) {
dir := "./test_table_clean_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := NewSchema("users", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "ID"},
{Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"},
})
table, err := db.CreateTable("users", schema)
if err != nil {
t.Fatal(err)
}
// 2. 插入数据
for i := 0; i < 100; i++ {
err := table.Insert(map[string]any{
"id": int64(i),
"name": "user" + string(rune(i)),
})
if err != nil {
t.Fatal(err)
}
}
// 3. 验证数据存在
stats := table.Stats()
t.Logf("Before Clean: %d rows", stats.TotalRows)
if stats.TotalRows == 0 {
t.Error("Expected data in table")
}
// 4. 清除数据
err = table.Clean()
if err != nil {
t.Fatal(err)
}
// 5. 验证数据已清除
stats = table.Stats()
t.Logf("After Clean: %d rows", stats.TotalRows)
if stats.TotalRows != 0 {
t.Errorf("Expected 0 rows after clean, got %d", stats.TotalRows)
}
// 6. 验证表仍然可用
err = table.Insert(map[string]any{
"id": int64(100),
"name": "new_user",
})
if err != nil {
t.Fatal(err)
}
stats = table.Stats()
if stats.TotalRows != 1 {
t.Errorf("Expected 1 row after insert, got %d", stats.TotalRows)
}
}
func TestTableDestroy(t *testing.T) {
dir := "./test_table_destroy_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := NewSchema("test", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
})
table, err := db.CreateTable("test", schema)
if err != nil {
t.Fatal(err)
}
// 2. 插入数据
for i := 0; i < 50; i++ {
table.Insert(map[string]any{"id": int64(i)})
}
// 3. 验证数据存在
stats := table.Stats()
t.Logf("Before Destroy: %d rows", stats.TotalRows)
if stats.TotalRows == 0 {
t.Error("Expected data in table")
}
// 4. 获取表目录路径
tableDir := table.dir
// 5. 销毁表
err = table.Destroy()
if err != nil {
t.Fatal(err)
}
// 6. 验证表目录已删除
if _, err := os.Stat(tableDir); !os.IsNotExist(err) {
t.Error("Table directory should be deleted")
}
// 7. 注意Table.Destroy() 只删除文件,不从 Database 中删除
// 表仍然在 Database 的元数据中,但文件已被删除
tables := db.ListTables()
found := false
for _, name := range tables {
if name == "test" {
found = true
break
}
}
if !found {
t.Error("Table should still be in database metadata (use Database.DestroyTable to remove from metadata)")
}
}
func TestTableCleanWithIndex(t *testing.T) {
dir := "./test_table_clean_index_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := NewSchema("users", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "ID"},
{Name: "email", Type: FieldTypeString, Indexed: true, Comment: "Email"},
{Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"},
})
table, err := db.CreateTable("users", schema)
if err != nil {
t.Fatal(err)
}
// 2. 创建索引
err = table.CreateIndex("id")
if err != nil {
t.Fatal(err)
}
err = table.CreateIndex("email")
if err != nil {
t.Fatal(err)
}
// 3. 插入数据
for i := 0; i < 50; i++ {
table.Insert(map[string]any{
"id": int64(i),
"email": "user" + string(rune(i)) + "@example.com",
"name": "User " + string(rune(i)),
})
}
// 4. 验证索引存在
indexes := table.ListIndexes()
if len(indexes) != 2 {
t.Errorf("Expected 2 indexes, got %d", len(indexes))
}
// 5. 清除数据
err = table.Clean()
if err != nil {
t.Fatal(err)
}
// 6. 验证数据已清除
stats := table.Stats()
if stats.TotalRows != 0 {
t.Errorf("Expected 0 rows after clean, got %d", stats.TotalRows)
}
// 7. 验证索引已被清除Clean 会删除索引数据)
indexes = table.ListIndexes()
if len(indexes) != 0 {
t.Logf("Note: Indexes were cleared (expected behavior), got %d", len(indexes))
}
// 8. 重新创建索引
table.CreateIndex("id")
table.CreateIndex("email")
// 9. 验证可以继续插入数据
err = table.Insert(map[string]any{
"id": int64(100),
"email": "new@example.com",
"name": "New User",
})
if err != nil {
t.Fatal(err)
}
stats = table.Stats()
if stats.TotalRows != 1 {
t.Errorf("Expected 1 row, got %d", stats.TotalRows)
}
}
func TestTableCleanAndQuery(t *testing.T) {
dir := "./test_table_clean_query_data"
defer os.RemoveAll(dir)
// 1. 创建数据库和表
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := NewSchema("test", []Field{
{Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"},
{Name: "status", Type: FieldTypeString, Indexed: false, Comment: "Status"},
})
table, err := db.CreateTable("test", schema)
if err != nil {
t.Fatal(err)
}
// 2. 插入数据
for i := 0; i < 30; i++ {
table.Insert(map[string]any{
"id": int64(i),
"status": "active",
})
}
// 3. 查询数据
rows, err := table.Query().Eq("status", "active").Rows()
if err != nil {
t.Fatal(err)
}
count := 0
for rows.Next() {
count++
}
rows.Close()
t.Logf("Before Clean: found %d rows", count)
if count != 30 {
t.Errorf("Expected 30 rows, got %d", count)
}
// 4. 清除数据
err = table.Clean()
if err != nil {
t.Fatal(err)
}
// 5. 再次查询
rows, err = table.Query().Eq("status", "active").Rows()
if err != nil {
t.Fatal(err)
}
count = 0
for rows.Next() {
count++
}
rows.Close()
t.Logf("After Clean: found %d rows", count)
if count != 0 {
t.Errorf("Expected 0 rows after clean, got %d", count)
}
// 6. 插入新数据并查询
table.Insert(map[string]any{
"id": int64(100),
"status": "active",
})
rows, err = table.Query().Eq("status", "active").Rows()
if err != nil {
t.Fatal(err)
}
count = 0
for rows.Next() {
count++
}
rows.Close()
if count != 1 {
t.Errorf("Expected 1 row, got %d", count)
}
}

View File

@@ -380,45 +380,40 @@ func (ui *WebUI) handleTableData(w http.ResponseWriter, r *http.Request, tableNa
} }
defer queryRows.Close() defer queryRows.Close()
// 收集所有 rows 到内存中用于分页 // 计算分页范围
allRows := make([]*srdb.SSTableRow, 0)
for queryRows.Next() {
row := queryRows.Row()
sstRow := &srdb.SSTableRow{
Seq: row.Data()["_seq"].(int64),
Time: row.Data()["_time"].(int64),
Data: make(map[string]any),
}
// 复制其他字段
for k, v := range row.Data() {
if k != "_seq" && k != "_time" {
sstRow.Data[k] = v
}
}
allRows = append(allRows, sstRow)
}
// 计算分页
totalRows := int64(len(allRows))
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
end := min(offset+pageSize, int(totalRows)) currentIndex := 0
// 获取当前页数据 // 直接在遍历时进行分页和字段处理
rows := make([]*srdb.SSTableRow, 0, pageSize) const maxStringLength = 100 // 最大字符串长度
if offset < int(totalRows) { data := make([]map[string]any, 0, pageSize)
rows = allRows[offset:end] totalRows := int64(0)
for queryRows.Next() {
totalRows++
// 跳过不在当前页的数据
if currentIndex < offset {
currentIndex++
continue
} }
// 构造响应,对 string 字段进行剪裁 // 已经收集够当前页的数据
const maxStringLength = 100 // 最大字符串长度 if len(data) >= pageSize {
data := make([]map[string]any, 0, len(rows)) continue
for _, row := range rows { }
rowData := make(map[string]any)
rowData["_seq"] = row.Seq row := queryRows.Row()
rowData["_time"] = row.Time rowData := make(map[string]any)
rowData["_seq"] = row.Data()["_seq"]
rowData["_time"] = row.Data()["_time"]
// 遍历所有字段并进行字符串截断
for k, v := range row.Data() {
if k == "_seq" || k == "_time" {
continue
}
// 遍历所有字段
for k, v := range row.Data {
// 检查字段类型 // 检查字段类型
field, err := tableSchema.GetField(k) field, err := tableSchema.GetField(k)
if err == nil && field.Type == srdb.FieldTypeString { if err == nil && field.Type == srdb.FieldTypeString {
@@ -434,7 +429,9 @@ func (ui *WebUI) handleTableData(w http.ResponseWriter, r *http.Request, tableNa
} }
rowData[k] = v rowData[k] = v
} }
data = append(data, rowData) data = append(data, rowData)
currentIndex++
} }
response := map[string]any{ response := map[string]any{