添加 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:
83
database.go
83
database.go
@@ -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
296
database_clean_test.go
Normal 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
195
database_table_ops_test.go
Normal 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
288
engine.go
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制切换 MemTable(switchMemTable 内部有锁)
|
||||||
|
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
244
engine_clean_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
table.go
19
table.go
@@ -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
314
table_clean_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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{
|
||||||
|
|||||||
Reference in New Issue
Block a user