From 77087d36c62499f0ad1f097148e876f1619ea139 Mon Sep 17 00:00:00 2001 From: bourdon Date: Fri, 10 Oct 2025 00:20:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=20Schema=20=E7=B3=BB=E7=BB=9F=E5=92=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=96=B0=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 Schema 支持更多数据类型(Duration、URL、JSON 等) - 优化 SSTable 编码解码性能 - 添加多个新示例程序: - all_types: 展示所有支持的数据类型 - new_types: 演示新增类型的使用 - struct_tags: 展示结构体标签功能 - time_duration: 时间和持续时间处理示例 - 完善测试用例和文档 - 优化代码结构和错误处理 --- CLAUDE.md | 130 ++- compaction_test.go | 14 +- database_test.go | 46 +- examples/all_types/data/sensors/CURRENT | 1 + .../all_types/data/sensors/MANIFEST-000001 | Bin 0 -> 79 bytes examples/all_types/data/sensors/schema.json | 34 + examples/all_types/data/sensors/wal/CURRENT | 1 + examples/all_types/main.go | 98 ++ examples/batch_insert/main.go | 42 +- examples/new_types/README.md | 129 +++ examples/new_types/data/api_logs/CURRENT | 1 + .../new_types/data/api_logs/MANIFEST-000001 | Bin 0 -> 79 bytes examples/new_types/data/api_logs/schema.json | 31 + examples/new_types/data/api_logs/wal/CURRENT | 1 + examples/new_types/go.mod | 15 + examples/new_types/go.sum | 6 + examples/new_types/main.go | 378 +++++++ examples/struct_tags/README.md | 132 +++ examples/struct_tags/data/database.meta | 10 + examples/struct_tags/data/users/CURRENT | 1 + .../struct_tags/data/users/MANIFEST-000001 | Bin 0 -> 79 bytes examples/struct_tags/data/users/schema.json | 66 ++ examples/struct_tags/data/users/wal/CURRENT | 1 + examples/struct_tags/go.mod | 15 + examples/struct_tags/go.sum | 6 + examples/struct_tags/main.go | 176 ++++ examples/time_duration/main.go | 140 +++ examples/time_duration_simple/main.go | 106 ++ examples/webui/commands/webui.go | 24 +- go.mod | 5 +- go.sum | 2 + index_btree_test.go | 18 +- index_test.go | 24 +- query_lazy_test.go | 24 +- schema.go | 929 ++++++++++++++++-- schema_test.go | 206 ++-- sstable.go | 428 +++++++- sstable_test.go | 52 +- table.go | 42 +- table_test.go | 124 +-- webui/webui.go | 2 +- 41 files changed, 3008 insertions(+), 452 deletions(-) create mode 100644 examples/all_types/data/sensors/CURRENT create mode 100644 examples/all_types/data/sensors/MANIFEST-000001 create mode 100644 examples/all_types/data/sensors/schema.json create mode 100644 examples/all_types/data/sensors/wal/CURRENT create mode 100644 examples/all_types/main.go create mode 100644 examples/new_types/README.md create mode 100644 examples/new_types/data/api_logs/CURRENT create mode 100644 examples/new_types/data/api_logs/MANIFEST-000001 create mode 100644 examples/new_types/data/api_logs/schema.json create mode 100644 examples/new_types/data/api_logs/wal/CURRENT create mode 100644 examples/new_types/go.mod create mode 100644 examples/new_types/go.sum create mode 100644 examples/new_types/main.go create mode 100644 examples/struct_tags/README.md create mode 100644 examples/struct_tags/data/database.meta create mode 100644 examples/struct_tags/data/users/CURRENT create mode 100644 examples/struct_tags/data/users/MANIFEST-000001 create mode 100644 examples/struct_tags/data/users/schema.json create mode 100644 examples/struct_tags/data/users/wal/CURRENT create mode 100644 examples/struct_tags/go.mod create mode 100644 examples/struct_tags/go.sum create mode 100644 examples/struct_tags/main.go create mode 100644 examples/time_duration/main.go create mode 100644 examples/time_duration_simple/main.go diff --git a/CLAUDE.md b/CLAUDE.md index 56b2127..bb14dbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,9 +131,9 @@ database_dir/ ```go schema := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, - {Name: "email", Type: FieldTypeString, Indexed: true, Comment: "邮箱(索引)"}, + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, + {Name: "email", Type: String, Indexed: true, Comment: "邮箱(索引)"}, }) table, _ := db.CreateTable("users", schema) @@ -142,7 +142,112 @@ table, _ := db.CreateTable("users", schema) - Schema 在 `Insert()` 时强制验证类型和必填字段 - 索引字段(`Indexed: true`)自动创建二级索引 - Schema 持久化到 `table_dir/schema.json`,包含校验和防篡改 -- 支持的类型:`FieldTypeString`, `FieldTypeInt64`, `FieldTypeBool`, `FieldTypeFloat` +- **支持的类型** (17 种,精确映射到 Go 基础类型): + - **有符号整数** (5种): `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - **无符号整数** (5种): `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64` + - **浮点数** (2种): `Float32`, `Float64` + - **字符串** (1种): `String` + - **布尔** (1种): `Bool` + - **特殊类型** (3种): `Byte` (独立类型,底层=uint8), `Rune` (独立类型,底层=int32), `Decimal` (高精度十进制,使用 shopspring/decimal) +- **Nullable 支持**: 字段可标记为 `Nullable: true`,允许 NULL 值 + +### 类型系统详解 + +**精确类型映射**: +从 v1.x 开始,SRDB 采用精确类型映射策略,每个 Go 基础类型都有对应的 FieldType。这带来以下优势: + +1. **存储优化**: 使用 `uint8` (1 字节) 存储百分比,而不是 `int64` (8 字节) +2. **语义明确**: `uint32` 表示设备ID,`float32` 表示传感器读数 +3. **类型安全**: 编译期和运行期双重类型检查 + +**类型转换规则**: + +```go +// 1. 相同类型:直接接受 +{Name: "age", Type: Int32} +Insert(map[string]any{"age": int32(25)}) // ✓ + +// 2. 兼容类型:自动转换(有符号 ↔ 无符号,需非负) +{Name: "count", Type: Int64} +Insert(map[string]any{"count": uint32(100)}) // ✓ + +// 3. 类型提升:整数 → 浮点 +{Name: "ratio", Type: Float32} +Insert(map[string]any{"ratio": int32(42)}) // ✓ 转为 42.0 + +// 4. JSON 兼容:float64 → 整数(需为整数值) +{Name: "id", Type: Int64} +Insert(map[string]any{"id": float64(123.0)}) // ✓ JSON 反序列化场景 + +// 5. 负数 → 无符号:拒绝 +{Name: "index", Type: Uint32} +Insert(map[string]any{"index": int32(-1)}) // ✗ 错误 +``` + +**最佳实践**: + +```go +// 推荐:根据数据范围选择合适的类型 +schema, _ := NewSchema("sensors", []Field{ + {Name: "device_id", Type: Uint32}, // 0 ~ 42亿 + {Name: "temperature", Type: Float32}, // 单精度足够 + {Name: "humidity", Type: Uint8}, // 0-100 + {Name: "status", Type: Bool}, // 布尔状态 +}) + +// 避免:盲目使用 int64 和 float64 +schema, _ := NewSchema("sensors_bad", []Field{ + {Name: "device_id", Type: Int64}, // 浪费 4 字节 + {Name: "temperature", Type: Float64}, // 浪费 4 字节 + {Name: "humidity", Type: Int64}, // 浪费 7 字节! + {Name: "status", Type: Int64}, // 浪费 7 字节! +}) +``` + +**新增类型的使用场景**: + +```go +// Byte 类型 - 状态码、标志位 +{Name: "status_code", Type: Byte, Comment: "HTTP 状态码 (0-255)"} +Insert(map[string]any{"status_code": uint8(200)}) // byte 和 uint8 底层相同 + +// Rune 类型 - 单个字符 +{Name: "grade", Type: Rune, Comment: "等级 (S/A/B/C)"} +Insert(map[string]any{"grade": rune('A')}) // rune 和 int32 底层相同 + +// Decimal 类型 - 金融计算 +{Name: "amount", Type: Decimal, Comment: "交易金额"} +import "github.com/shopspring/decimal" +Insert(map[string]any{"amount": decimal.NewFromFloat(123.456)}) + +// Nullable 支持 - 可选字段 +{Name: "email", Type: String, Nullable: true, Comment: "邮箱(可选)"} +Insert(map[string]any{"email": nil}) // 允许 NULL +Insert(map[string]any{"email": "user@example.com"}) // 或有值 +``` + +**从结构体自动生成 Schema**: + +```go +type Sensor struct { + DeviceID uint32 `srdb:"device_id;indexed;comment:设备ID"` + Temperature float32 `srdb:"temperature;comment:温度"` + Humidity uint8 `srdb:"humidity;comment:湿度 0-100"` + Online bool `srdb:"online;comment:是否在线"` +} + +// 自动映射: +// uint32 → Uint32 +// float32 → Float32 +// uint8 → Uint8 (也可用 byte) +// bool → Bool +fields, _ := StructToFields(Sensor{}) +schema, _ := NewSchema("sensors", fields) +``` + +**参考示例**: +- `examples/all_types/` - 展示所有 17 种类型的基本使用 +- `examples/new_types/` - 展示新增的 Byte、Rune、Decimal 类型和 Nullable 支持的实际应用场景 ### Query Builder @@ -292,12 +397,25 @@ Magic (4B) | Seq (8B) | Time (8B) | FieldCount (2B) | - **Schema 是强制的**: 所有表必须定义 Schema,不再支持无 Schema 模式 - **索引非自动创建**: 需要在 Schema 中显式标记 `Indexed: true` -- **类型严格**: Schema 验证严格,int 和 int64 需要正确匹配 +- **类型名称简化**: + - ⚠️ **重要变更**: 从 v2.0 开始,类型名称已简化,使用简短形式(如 `String` 而非 `FieldTypeString`) + - 每个 Go 类型有对应的简短常量(如 `int32` → `Int32`,`string` → `String`) + - 插入时类型会自动转换(如 `int` → `int32`),但需要注意负数不能转为无符号类型 +- **新增类型的使用**: + - **Byte**: 虽然底层是 `uint8`,但在 Schema 中作为独立类型,语义更清晰(用于状态码、标志位等) + - **Rune**: 虽然底层是 `int32`,但在 Schema 中作为独立类型,用于存储单个 Unicode 字符 + - **Decimal**: 必须使用 `github.com/shopspring/decimal` 包,用于金融计算等需要精确数值的场景 +- **Nullable 支持**: + - 需要显式标记 `Nullable: true`,默认字段不允许 NULL + - NULL 值在 Go 中表示为 `nil` + - 读取时需要检查值是否存在且不为 nil +- **选择合适的类型大小**: + - 避免盲目使用 `Int64`/`Float64`,根据数据范围选择(如百分比用 `Uint8`,状态码用 `Byte`) + - 过大的类型浪费存储和内存,影响性能 - **Compaction 磁盘占用**: 合并期间旧文件和新文件共存,会暂时增加磁盘使用 - **MemTable flush 异步**: 关闭时需要等待 immutable flush 完成 - **mmap 虚拟内存**: 可能显示较大的虚拟内存使用(正常,OS 管理,不是实际 RAM) - **无 panic**: 所有 panic 已替换为错误返回,需要正确处理错误 -- **废弃代码**: `SSTableCompressionNone` 等常量已删除 ## Web UI diff --git a/compaction_test.go b/compaction_test.go index 372b1ba..0d2e659 100644 --- a/compaction_test.go +++ b/compaction_test.go @@ -21,7 +21,7 @@ func TestCompactionBasic(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "value", Type: FieldTypeInt64}, + {Name: "value", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -226,7 +226,7 @@ func TestCompactionMerge(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "value", Type: FieldTypeString}, + {Name: "value", Type: String}, }) if err != nil { t.Fatal(err) @@ -350,7 +350,7 @@ func BenchmarkCompaction(b *testing.B) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "value", Type: FieldTypeString}, + {Name: "value", Type: String}, }) if err != nil { b.Fatal(err) @@ -439,10 +439,10 @@ func TestCompactionQueryOrder(t *testing.T) { // 创建 Schema - 包含多个字段以增加数据大小 schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "name", Type: FieldTypeString}, - {Name: "data", Type: FieldTypeString}, - {Name: "timestamp", Type: FieldTypeInt64}, + {Name: "id", Type: Int64}, + {Name: "name", Type: String}, + {Name: "data", Type: String}, + {Name: "timestamp", Type: Int64}, }) if err != nil { t.Fatal(err) diff --git a/database_test.go b/database_test.go index 92d1141..ccab7d6 100644 --- a/database_test.go +++ b/database_test.go @@ -41,8 +41,8 @@ func TestCreateTable(t *testing.T) { // 创建 Schema userSchema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -80,16 +80,16 @@ func TestMultipleTables(t *testing.T) { // 创建多个表 userSchema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, }) if err != nil { t.Fatal(err) } orderSchema, err := NewSchema("orders", []Field{ - {Name: "order_id", Type: FieldTypeString, Indexed: true, Comment: "订单ID"}, - {Name: "amount", Type: FieldTypeInt64, Indexed: true, Comment: "金额"}, + {Name: "order_id", Type: String, Indexed: true, Comment: "订单ID"}, + {Name: "amount", Type: Int64, Indexed: true, Comment: "金额"}, }) if err != nil { t.Fatal(err) @@ -127,8 +127,8 @@ func TestTableOperations(t *testing.T) { // 创建表 userSchema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -192,7 +192,7 @@ func TestDropTable(t *testing.T) { // 创建表 userSchema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, }) if err != nil { t.Fatal(err) @@ -230,8 +230,8 @@ func TestDatabaseRecover(t *testing.T) { } userSchema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -291,8 +291,8 @@ func TestDatabaseClean(t *testing.T) { // 2. 创建多个表并插入数据 // 表 1: users usersSchema, err := NewSchema("users", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "User ID"}, - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"}, + {Name: "id", Type: Int64, Indexed: true, Comment: "User ID"}, + {Name: "name", Type: String, Indexed: false, Comment: "Name"}, }) if err != nil { t.Fatal(err) @@ -311,8 +311,8 @@ func TestDatabaseClean(t *testing.T) { // 表 2: orders ordersSchema, err := NewSchema("orders", []Field{ - {Name: "order_id", Type: FieldTypeInt64, Indexed: true, Comment: "Order ID"}, - {Name: "amount", Type: FieldTypeInt64, Indexed: false, Comment: "Amount"}, + {Name: "order_id", Type: Int64, Indexed: true, Comment: "Order ID"}, + {Name: "amount", Type: Int64, Indexed: false, Comment: "Amount"}, }) if err != nil { t.Fatal(err) @@ -392,7 +392,7 @@ func TestDatabaseDestroy(t *testing.T) { } schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, }) if err != nil { t.Fatal(err) @@ -448,8 +448,8 @@ func TestDatabaseCleanMultipleTables(t *testing.T) { for i := range 5 { tableName := fmt.Sprintf("table%d", i) schema, err := NewSchema(tableName, []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, - {Name: "value", Type: FieldTypeString, Indexed: false, Comment: "Value"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, + {Name: "value", Type: String, Indexed: false, Comment: "Value"}, }) if err != nil { t.Fatal(err) @@ -524,7 +524,7 @@ func TestDatabaseCleanAndReopen(t *testing.T) { } schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, }) if err != nil { t.Fatal(err) @@ -594,8 +594,8 @@ func TestDatabaseCleanTable(t *testing.T) { defer db.Close() schema, err := NewSchema("users", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, + {Name: "name", Type: String, Indexed: false, Comment: "Name"}, }) if err != nil { t.Fatal(err) @@ -666,7 +666,7 @@ func TestDatabaseDestroyTable(t *testing.T) { defer db.Close() schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, }) if err != nil { t.Fatal(err) @@ -721,7 +721,7 @@ func TestDatabaseDestroyTableMultiple(t *testing.T) { defer db.Close() schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, }) if err != nil { t.Fatal(err) diff --git a/examples/all_types/data/sensors/CURRENT b/examples/all_types/data/sensors/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/examples/all_types/data/sensors/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/examples/all_types/data/sensors/MANIFEST-000001 b/examples/all_types/data/sensors/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..6c53b5f00063d812c2e28fabae4cf694cbcd4c87 GIT binary patch literal 79 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq` %s\n", i+1, t.name, t.goType, t.srdbType.String()) + } +} + +func sensorDataExample() { + // 创建 Schema + schema, err := srdb.NewSchema("sensors", []srdb.Field{ + {Name: "device_id", Type: srdb.Uint32, Indexed: true, Comment: "设备ID"}, + {Name: "temperature", Type: srdb.Float32, Comment: "温度(摄氏度)"}, + {Name: "humidity", Type: srdb.Uint8, Comment: "湿度(0-100)"}, + {Name: "online", Type: srdb.Bool, Comment: "是否在线"}, + }) + if err != nil { + log.Fatal(err) + } + + table, err := srdb.OpenTable(&srdb.TableOptions{ + Dir: "./data/sensors", + Name: schema.Name, + Fields: schema.Fields, + }) + if err != nil { + log.Fatal(err) + } + defer table.Close() + + // 插入数据 + sensors := []map[string]any{ + {"device_id": uint32(1001), "temperature": float32(23.5), "humidity": uint8(65), "online": true}, + {"device_id": uint32(1002), "temperature": float32(18.2), "humidity": uint8(72), "online": true}, + {"device_id": uint32(1003), "temperature": float32(25.8), "humidity": uint8(58), "online": false}, + } + + err = table.Insert(sensors) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("✓ 插入 %d 个传感器数据\n", len(sensors)) + fmt.Println("\n类型优势演示:") + fmt.Println(" - device_id 使用 uint32 (节省空间,支持 42 亿设备)") + fmt.Println(" - temperature 使用 float32 (单精度足够,节省 50% 空间)") + fmt.Println(" - humidity 使用 uint8 (0-100 范围,仅需 1 字节)") + fmt.Println(" - online 使用 bool (语义清晰)") +} diff --git a/examples/batch_insert/main.go b/examples/batch_insert/main.go index b6b8b51..2ad6948 100644 --- a/examples/batch_insert/main.go +++ b/examples/batch_insert/main.go @@ -56,8 +56,8 @@ func example1() { fmt.Println("=== 示例 1: 插入单个 map ===") schema, err := srdb.NewSchema("users", []srdb.Field{ - {Name: "name", Type: srdb.FieldTypeString, Comment: "用户名"}, - {Name: "age", Type: srdb.FieldTypeInt64, Comment: "年龄"}, + {Name: "name", Type: srdb.String, Comment: "用户名"}, + {Name: "age", Type: srdb.Int64, Comment: "年龄"}, }) if err != nil { log.Fatal(err) @@ -93,9 +93,9 @@ func example2() { fmt.Println("=== 示例 2: 批量插入 map 切片 ===") schema, err := srdb.NewSchema("users", []srdb.Field{ - {Name: "name", Type: srdb.FieldTypeString}, - {Name: "age", Type: srdb.FieldTypeInt64}, - {Name: "email", Type: srdb.FieldTypeString, Indexed: true}, + {Name: "name", Type: srdb.String}, + {Name: "age", Type: srdb.Int64}, + {Name: "email", Type: srdb.String, Indexed: true}, }) if err != nil { log.Fatal(err) @@ -141,10 +141,10 @@ func example3() { fmt.Println("=== 示例 3: 插入单个结构体 ===") schema, err := srdb.NewSchema("users", []srdb.Field{ - {Name: "name", Type: srdb.FieldTypeString}, - {Name: "age", Type: srdb.FieldTypeInt64}, - {Name: "email", Type: srdb.FieldTypeString}, - {Name: "is_active", Type: srdb.FieldTypeBool}, + {Name: "name", Type: srdb.String}, + {Name: "age", Type: srdb.Int64}, + {Name: "email", Type: srdb.String}, + {Name: "is_active", Type: srdb.Bool}, }) if err != nil { log.Fatal(err) @@ -185,10 +185,10 @@ func example4() { fmt.Println("=== 示例 4: 批量插入结构体切片 ===") schema, err := srdb.NewSchema("users", []srdb.Field{ - {Name: "name", Type: srdb.FieldTypeString}, - {Name: "age", Type: srdb.FieldTypeInt64}, - {Name: "email", Type: srdb.FieldTypeString}, - {Name: "is_active", Type: srdb.FieldTypeBool}, + {Name: "name", Type: srdb.String}, + {Name: "age", Type: srdb.Int64}, + {Name: "email", Type: srdb.String}, + {Name: "is_active", Type: srdb.Bool}, }) if err != nil { log.Fatal(err) @@ -235,10 +235,10 @@ func example5() { fmt.Println("=== 示例 5: 批量插入结构体指针切片 ===") schema, err := srdb.NewSchema("users", []srdb.Field{ - {Name: "name", Type: srdb.FieldTypeString}, - {Name: "age", Type: srdb.FieldTypeInt64}, - {Name: "email", Type: srdb.FieldTypeString}, - {Name: "is_active", Type: srdb.FieldTypeBool}, + {Name: "name", Type: srdb.String}, + {Name: "age", Type: srdb.Int64}, + {Name: "email", Type: srdb.String}, + {Name: "is_active", Type: srdb.Bool}, }) if err != nil { log.Fatal(err) @@ -278,10 +278,10 @@ func example6() { fmt.Println("=== 示例 6: 使用 snake_case 自动转换 ===") schema, err := srdb.NewSchema("products", []srdb.Field{ - {Name: "product_id", Type: srdb.FieldTypeString, Comment: "产品ID"}, - {Name: "product_name", Type: srdb.FieldTypeString, Comment: "产品名称"}, - {Name: "price", Type: srdb.FieldTypeFloat, Comment: "价格"}, - {Name: "in_stock", Type: srdb.FieldTypeBool, Comment: "是否有货"}, + {Name: "product_id", Type: srdb.String, Comment: "产品ID"}, + {Name: "product_name", Type: srdb.String, Comment: "产品名称"}, + {Name: "price", Type: srdb.Float64, Comment: "价格"}, + {Name: "in_stock", Type: srdb.Bool, Comment: "是否有货"}, }) if err != nil { log.Fatal(err) diff --git a/examples/new_types/README.md b/examples/new_types/README.md new file mode 100644 index 0000000..55b4592 --- /dev/null +++ b/examples/new_types/README.md @@ -0,0 +1,129 @@ +# SRDB 新类型系统示例 + +本示例展示 SRDB 最新的类型系统特性,包括新增的 **Byte**、**Rune**、**Decimal** 类型以及 **Nullable** 支持。 + +## 新增特性 + +### 1. Byte 类型 (FieldTypeByte) +- **用途**: 存储 0-255 范围的小整数 +- **适用场景**: HTTP 状态码、标志位、小范围枚举值 +- **优势**: 仅占 1 字节,相比 int64 节省 87.5% 空间 + +### 2. Rune 类型 (FieldTypeRune) +- **用途**: 存储单个 Unicode 字符 +- **适用场景**: 等级标识(S/A/B/C)、单字符代码、Unicode 字符 +- **优势**: 语义清晰,支持所有 Unicode 字符 + +### 3. Decimal 类型 (FieldTypeDecimal) +- **用途**: 高精度十进制数值 +- **适用场景**: 金融计算、科学计算、需要精确数值的场景 +- **优势**: 无精度损失,避免浮点数误差 +- **实现**: 使用 `github.com/shopspring/decimal` 库 + +### 4. Nullable 支持 +- **用途**: 允许字段值为 NULL +- **适用场景**: 可选字段、区分"未填写"和"空值" +- **使用**: 在 Field 定义中设置 `Nullable: true` + +## 完整类型系统 + +SRDB 现在支持 **17 种**数据类型: + +| 类别 | 类型 | 说明 | +|------|------|------| +| 有符号整数 | int, int8, int16, int32, int64 | 5 种 | +| 无符号整数 | uint, uint8, uint16, uint32, uint64 | 5 种 | +| 浮点 | float32, float64 | 2 种 | +| 字符串 | string | 1 种 | +| 布尔 | bool | 1 种 | +| 特殊类型 | byte, rune, decimal | 3 种 | + +## 运行示例 + +```bash +cd examples/new_types +go run main.go +``` + +## 示例说明 + +### 示例 1: Byte 类型(API 日志) +演示使用 `byte` 类型存储 HTTP 状态码,节省存储空间。 + +```go +{Name: "status_code", Type: srdb.FieldTypeByte, Comment: "HTTP 状态码"} +``` + +### 示例 2: Rune 类型(用户等级) +演示使用 `rune` 类型存储等级字符(S/A/B/C/D)。 + +```go +{Name: "level", Type: srdb.FieldTypeRune, Comment: "等级字符"} +``` + +### 示例 3: Decimal 类型(金融交易) +演示使用 `decimal` 类型进行精确的金融计算。 + +```go +{Name: "amount", Type: srdb.FieldTypeDecimal, Comment: "交易金额(高精度)"} + +// 使用示例 +amount := decimal.NewFromFloat(1234.56789012345) +fee := decimal.NewFromFloat(1.23) +total := amount.Add(fee) // 精确加法,无误差 +``` + +### 示例 4: Nullable 支持(用户资料) +演示可选字段的使用,允许某些字段为 NULL。 + +```go +{Name: "email", Type: srdb.FieldTypeString, Nullable: true, Comment: "邮箱(可选)"} + +// 插入数据时可以为 nil +{"username": "Bob", "email": nil} // email 为 NULL +``` + +### 示例 5: 完整类型系统 +演示所有 17 种类型在同一个表中的使用。 + +## 类型优势对比 + +| 场景 | 旧方案 | 新方案 | 优势 | +|------|--------|--------|------| +| HTTP 状态码 | int64 (8 字节) | byte (1 字节) | 节省 87.5% 空间 | +| 等级标识 | string ("S") | rune ('S') | 更精确的语义 | +| 金融金额 | float64 (有误差) | decimal (无误差) | 精确计算 | +| 可选字段 | 空字符串 "" | NULL | 区分未填写和空值 | + +## 注意事项 + +1. **Byte 和 Rune 的底层类型** + - `byte` 在 Go 中等同于 `uint8` + - `rune` 在 Go 中等同于 `int32` + - 但在 SRDB Schema 中作为独立类型,语义更清晰 + +2. **Decimal 的使用** + - 需要导入 `github.com/shopspring/decimal` + - 创建方式:`decimal.NewFromFloat()`, `decimal.NewFromString()`, `decimal.NewFromInt()` + - 运算方法:`Add()`, `Sub()`, `Mul()`, `Div()` 等 + +3. **Nullable 的使用** + - NULL 值在 Go 中表示为 `nil` + - 读取时需要检查值是否存在且不为 nil + - 非 Nullable 字段不允许为 NULL,会在验证时报错 + +## 最佳实践 + +1. **选择合适的类型** + - 使用最小的整数类型来节省空间(如 uint8 而非 int64) + - 金融计算必须使用 decimal,避免 float64 + - 可选字段使用 Nullable,而非空字符串或特殊值 + +2. **性能优化** + - 小整数类型(byte, int8, uint16)可减少存储和传输开销 + - 索引字段选择合适的类型(如 byte 类型的索引比 string 更高效) + +3. **数据完整性** + - 必填字段设置 `Nullable: false` + - 使用类型系统保证数据正确性 + - Decimal 类型保证金融数据精确性 diff --git a/examples/new_types/data/api_logs/CURRENT b/examples/new_types/data/api_logs/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/examples/new_types/data/api_logs/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/examples/new_types/data/api_logs/MANIFEST-000001 b/examples/new_types/data/api_logs/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..6c53b5f00063d812c2e28fabae4cf694cbcd4c87 GIT binary patch literal 79 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq` ../.. + +require ( + code.tczkiot.com/wlw/srdb v0.0.0-00010101000000-000000000000 + github.com/shopspring/decimal v1.4.0 +) + +require ( + github.com/edsrzf/mmap-go v1.1.0 // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect +) diff --git a/examples/new_types/go.sum b/examples/new_types/go.sum new file mode 100644 index 0000000..79975e3 --- /dev/null +++ b/examples/new_types/go.sum @@ -0,0 +1,6 @@ +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/examples/new_types/main.go b/examples/new_types/main.go new file mode 100644 index 0000000..4bde748 --- /dev/null +++ b/examples/new_types/main.go @@ -0,0 +1,378 @@ +package main + +import ( + "fmt" + "log" + "os" + + "code.tczkiot.com/wlw/srdb" + "github.com/shopspring/decimal" +) + +func main() { + fmt.Println("=== SRDB 新类型系统示例 ===") + + // 清理旧数据 + os.RemoveAll("./data") + + // 示例 1: Byte 类型 - 适用于状态码、标志位等小整数 + fmt.Println("\n=== 示例 1: Byte 类型(状态码)===") + byteExample() + + // 示例 2: Rune 类型 - 适用于单个字符、等级标识等 + fmt.Println("\n=== 示例 2: Rune 类型(等级字符)===") + runeExample() + + // 示例 3: Decimal 类型 - 适用于金融计算、精确数值 + fmt.Println("\n=== 示例 3: Decimal 类型(金融数据)===") + decimalExample() + + // 示例 4: Nullable 支持 - 允许字段为 NULL + fmt.Println("\n=== 示例 4: Nullable 支持 ===") + nullableExample() + + // 示例 5: 完整类型系统 - 展示所有 17 种类型 + fmt.Println("\n=== 示例 5: 完整类型系统(17 种类型)===") + allTypesExample() + + fmt.Println("\n✓ 所有示例执行成功!") +} + +// byteExample 演示 Byte 类型的使用 +func byteExample() { + // 创建 Schema - 使用 byte 类型存储状态码 + schema, err := srdb.NewSchema("api_logs", []srdb.Field{ + {Name: "endpoint", Type: srdb.String, Comment: "API 端点"}, + {Name: "status_code", Type: srdb.Byte, Comment: "HTTP 状态码(用 byte 节省空间)"}, + {Name: "response_time_ms", Type: srdb.Uint16, Comment: "响应时间(毫秒)"}, + }) + if err != nil { + log.Fatal(err) + } + + table, err := srdb.OpenTable(&srdb.TableOptions{ + Dir: "./data/api_logs", + Name: schema.Name, + Fields: schema.Fields, + }) + if err != nil { + log.Fatal(err) + } + defer table.Close() + + // 插入数据 - status_code 使用 byte 类型(0-255) + logs := []map[string]any{ + {"endpoint": "/api/users", "status_code": uint8(200), "response_time_ms": uint16(45)}, + {"endpoint": "/api/orders", "status_code": uint8(201), "response_time_ms": uint16(89)}, + {"endpoint": "/api/products", "status_code": uint8(255), "response_time_ms": uint16(12)}, + {"endpoint": "/api/auth", "status_code": uint8(128), "response_time_ms": uint16(234)}, + } + + err = table.Insert(logs) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("✓ 插入 %d 条 API 日志\n", len(logs)) + fmt.Println("类型优势:") + fmt.Println(" - status_code 使用 byte (仅 1 字节,相比 int64 节省 87.5% 空间)") + fmt.Println(" - response_time_ms 使用 uint16 (0-65535ms 范围足够)") + + // 查询数据 + row, _ := table.Get(1) + fmt.Printf("\n查询结果: endpoint=%s, status_code=%d, response_time=%dms\n", + row.Data["endpoint"], row.Data["status_code"], row.Data["response_time_ms"]) +} + +// runeExample 演示 Rune 类型的使用 +func runeExample() { + // 创建 Schema - 使用 rune 类型存储等级字符 + schema, err := srdb.NewSchema("user_levels", []srdb.Field{ + {Name: "username", Type: srdb.String, Indexed: true, Comment: "用户名"}, + {Name: "level", Type: srdb.Rune, Comment: "等级字符 (S/A/B/C/D)"}, + {Name: "score", Type: srdb.Uint32, Comment: "积分"}, + }) + if err != nil { + log.Fatal(err) + } + + table, err := srdb.OpenTable(&srdb.TableOptions{ + Dir: "./data/user_levels", + Name: schema.Name, + Fields: schema.Fields, + }) + if err != nil { + log.Fatal(err) + } + defer table.Close() + + // 插入数据 - level 使用 rune 类型存储单个字符 + users := []map[string]any{ + {"username": "Alice", "level": rune('S'), "score": uint32(9500)}, + {"username": "Bob", "level": rune('A'), "score": uint32(7200)}, + {"username": "Charlie", "level": rune('B'), "score": uint32(5800)}, + {"username": "David", "level": rune('C'), "score": uint32(3400)}, + } + + err = table.Insert(users) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("✓ 插入 %d 个用户等级数据\n", len(users)) + fmt.Println("类型优势:") + fmt.Println(" - level 使用 rune 存储单个字符(语义清晰)") + fmt.Println(" - 支持 Unicode 字符,如中文等级:'甲'、'乙'、'丙'") + + // 查询数据 + row, _ := table.Get(1) + levelRune := row.Data["level"].(rune) + fmt.Printf("\n查询结果: username=%s, level=%c, score=%d\n", + row.Data["username"], levelRune, row.Data["score"]) +} + +// decimalExample 演示 Decimal 类型的使用 +func decimalExample() { + // 创建 Schema - 使用 decimal 类型存储金融数据 + schema, err := srdb.NewSchema("transactions", []srdb.Field{ + {Name: "tx_id", Type: srdb.String, Indexed: true, Comment: "交易ID"}, + {Name: "amount", Type: srdb.Decimal, Comment: "交易金额(高精度)"}, + {Name: "fee", Type: srdb.Decimal, Comment: "手续费(高精度)"}, + {Name: "currency", Type: srdb.String, Comment: "货币类型"}, + }) + if err != nil { + log.Fatal(err) + } + + table, err := srdb.OpenTable(&srdb.TableOptions{ + Dir: "./data/transactions", + Name: schema.Name, + Fields: schema.Fields, + }) + if err != nil { + log.Fatal(err) + } + defer table.Close() + + // 插入数据 - amount 和 fee 使用 decimal 类型(无精度损失) + transactions := []map[string]any{ + { + "tx_id": "TX001", + "amount": decimal.NewFromFloat(1234.56789012345), // 高精度 + "fee": decimal.NewFromFloat(1.23), + "currency": "USD", + }, + { + "tx_id": "TX002", + "amount": decimal.RequireFromString("9876.543210987654321"), // 字符串创建,更精确 + "fee": decimal.NewFromFloat(9.88), + "currency": "EUR", + }, + { + "tx_id": "TX003", + "amount": decimal.NewFromFloat(0.00000001), // 极小值 + "fee": decimal.NewFromFloat(0.0000001), + "currency": "BTC", + }, + } + + err = table.Insert(transactions) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("✓ 插入 %d 笔交易\n", len(transactions)) + fmt.Println("类型优势:") + fmt.Println(" - decimal 类型无精度损失(使用 shopspring/decimal)") + fmt.Println(" - 适合金融计算、科学计算等需要精确数值的场景") + fmt.Println(" - 避免浮点数运算误差(如 0.1 + 0.2 ≠ 0.3)") + + // 查询数据并进行计算 + row, _ := table.Get(1) + amount := row.Data["amount"].(decimal.Decimal) + fee := row.Data["fee"].(decimal.Decimal) + total := amount.Add(fee) // decimal 类型的精确加法 + + fmt.Printf("\n查询结果: tx_id=%s, currency=%s\n", row.Data["tx_id"], row.Data["currency"]) + fmt.Printf(" 金额: %s\n", amount.String()) + fmt.Printf(" 手续费: %s\n", fee.String()) + fmt.Printf(" 总计: %s (精确计算,无误差)\n", total.String()) +} + +// nullableExample 演示 Nullable 支持 +func nullableExample() { + // 创建 Schema - 某些字段允许为 NULL + schema, err := srdb.NewSchema("user_profiles", []srdb.Field{ + {Name: "username", Type: srdb.String, Nullable: false, Comment: "用户名(必填)"}, + {Name: "email", Type: srdb.String, Nullable: true, Comment: "邮箱(可选)"}, + {Name: "age", Type: srdb.Uint8, Nullable: true, Comment: "年龄(可选)"}, + {Name: "bio", Type: srdb.String, Nullable: true, Comment: "个人简介(可选)"}, + }) + if err != nil { + log.Fatal(err) + } + + table, err := srdb.OpenTable(&srdb.TableOptions{ + Dir: "./data/user_profiles", + Name: schema.Name, + Fields: schema.Fields, + }) + if err != nil { + log.Fatal(err) + } + defer table.Close() + + // 插入数据 - 可选字段可以为 nil + profiles := []map[string]any{ + { + "username": "Alice", + "email": "alice@example.com", + "age": uint8(25), + "bio": "Hello, I'm Alice!", + }, + { + "username": "Bob", + "email": nil, // email 为 NULL + "age": uint8(30), + "bio": "Software Engineer", + }, + { + "username": "Charlie", + "email": "charlie@example.com", + "age": nil, // age 为 NULL + "bio": nil, // bio 为 NULL + }, + } + + err = table.Insert(profiles) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("✓ 插入 %d 个用户资料(包含 NULL 值)\n", len(profiles)) + fmt.Println("类型优势:") + fmt.Println(" - Nullable 字段可以为 NULL,区分'未填写'和'空字符串'") + fmt.Println(" - 非 Nullable 字段必须有值,保证数据完整性") + + // 查询数据 + for i := 1; i <= 3; i++ { + row, _ := table.Get(int64(i)) + fmt.Printf("\n用户 %d: username=%s", i, row.Data["username"]) + if email, ok := row.Data["email"]; ok && email != nil { + fmt.Printf(", email=%s", email) + } else { + fmt.Print(", email=NULL") + } + if age, ok := row.Data["age"]; ok && age != nil { + fmt.Printf(", age=%d", age) + } else { + fmt.Print(", age=NULL") + } + } + fmt.Println() +} + +// allTypesExample 展示所有 17 种类型 +func allTypesExample() { + schema, err := srdb.NewSchema("all_types_demo", []srdb.Field{ + // 有符号整数类型 (5 种) + {Name: "f_int", Type: srdb.Int, Comment: "int"}, + {Name: "f_int8", Type: srdb.Int8, Comment: "int8"}, + {Name: "f_int16", Type: srdb.Int16, Comment: "int16"}, + {Name: "f_int32", Type: srdb.Int32, Comment: "int32"}, + {Name: "f_int64", Type: srdb.Int64, Comment: "int64"}, + + // 无符号整数类型 (5 种) + {Name: "f_uint", Type: srdb.Uint, Comment: "uint"}, + {Name: "f_uint8", Type: srdb.Uint8, Comment: "uint8"}, + {Name: "f_uint16", Type: srdb.Uint16, Comment: "uint16"}, + {Name: "f_uint32", Type: srdb.Uint32, Comment: "uint32"}, + {Name: "f_uint64", Type: srdb.Uint64, Comment: "uint64"}, + + // 浮点类型 (2 种) + {Name: "f_float32", Type: srdb.Float32, Comment: "float32"}, + {Name: "f_float64", Type: srdb.Float64, Comment: "float64"}, + + // 字符串类型 (1 种) + {Name: "f_string", Type: srdb.String, Comment: "string"}, + + // 布尔类型 (1 种) + {Name: "f_bool", Type: srdb.Bool, Comment: "bool"}, + + // 特殊类型 (3 种) + {Name: "f_byte", Type: srdb.Byte, Comment: "byte (=uint8)"}, + {Name: "f_rune", Type: srdb.Rune, Comment: "rune (=int32)"}, + {Name: "f_decimal", Type: srdb.Decimal, Comment: "decimal (高精度)"}, + }) + if err != nil { + log.Fatal(err) + } + + table, err := srdb.OpenTable(&srdb.TableOptions{ + Dir: "./data/all_types", + Name: schema.Name, + Fields: schema.Fields, + }) + if err != nil { + log.Fatal(err) + } + defer table.Close() + + // 插入包含所有类型的数据 + record := map[string]any{ + // 有符号整数 + "f_int": int(-12345), + "f_int8": int8(-128), + "f_int16": int16(-32768), + "f_int32": int32(-2147483648), + "f_int64": int64(-9223372036854775808), + + // 无符号整数 + "f_uint": uint(12345), + "f_uint8": uint8(255), + "f_uint16": uint16(65535), + "f_uint32": uint32(4294967295), + "f_uint64": uint64(18446744073709551615), + + // 浮点 + "f_float32": float32(3.14159), + "f_float64": float64(2.718281828459045), + + // 字符串 + "f_string": "Hello, SRDB! 你好!", + + // 布尔 + "f_bool": true, + + // 特殊类型 + "f_byte": byte(255), + "f_rune": rune('中'), + "f_decimal": decimal.NewFromFloat(123456.789012345), + } + + err = table.Insert(record) + if err != nil { + log.Fatal(err) + } + + fmt.Println("✓ 插入包含所有 17 种类型的数据") + fmt.Println("\nSRDB 完整类型系统:") + fmt.Println(" 有符号整数: int, int8, int16, int32, int64 (5 种)") + fmt.Println(" 无符号整数: uint, uint8, uint16, uint32, uint64 (5 种)") + fmt.Println(" 浮点类型: float32, float64 (2 种)") + fmt.Println(" 字符串类型: string (1 种)") + fmt.Println(" 布尔类型: bool (1 种)") + fmt.Println(" 特殊类型: byte, rune, decimal (3 种)") + fmt.Println(" 总计: 17 种类型") + + // 查询并验证数据 + row, _ := table.Get(1) + fmt.Println("\n数据验证:") + fmt.Printf(" f_int=%d, f_int64=%d\n", row.Data["f_int"], row.Data["f_int64"]) + fmt.Printf(" f_uint=%d, f_uint64=%d\n", row.Data["f_uint"], row.Data["f_uint64"]) + fmt.Printf(" f_float32=%f, f_float64=%f\n", row.Data["f_float32"], row.Data["f_float64"]) + fmt.Printf(" f_string=%s\n", row.Data["f_string"]) + fmt.Printf(" f_bool=%v\n", row.Data["f_bool"]) + fmt.Printf(" f_byte=%d, f_rune=%c\n", row.Data["f_byte"], row.Data["f_rune"]) + fmt.Printf(" f_decimal=%s\n", row.Data["f_decimal"].(decimal.Decimal).String()) +} diff --git a/examples/struct_tags/README.md b/examples/struct_tags/README.md new file mode 100644 index 0000000..090a0ee --- /dev/null +++ b/examples/struct_tags/README.md @@ -0,0 +1,132 @@ +# Struct Tags 示例 + +本示例展示如何使用 Go struct tags 来定义 SRDB Schema,包括完整的 nullable 支持。 + +## 功能特性 + +### Struct Tag 格式 + +SRDB 支持以下 struct tag 格式: + +```go +type User struct { + // 基本格式 + Name string `srdb:"name"` + + // 指定索引 + Email string `srdb:"email;indexed"` + + // 标记为可空 + Bio string `srdb:"bio;nullable"` + + // 可空 + 索引 + Phone string `srdb:"phone;nullable;indexed"` + + // 完整格式 + Age int64 `srdb:"age;indexed;nullable;comment:用户年龄"` + + // 忽略字段 + TempData string `srdb:"-"` +} +``` + +### Tag 说明 + +- **字段名**: 第一部分指定数据库字段名(可选,默认自动转换为 snake_case) +- **indexed**: 标记该字段需要建立索引 +- **nullable**: 标记该字段允许 NULL 值 +- **comment**: 指定字段注释 +- **-**: 忽略该字段(不包含在 Schema 中) + +## 运行示例 + +```bash +cd examples/struct_tags +go run main.go +``` + +## 示例输出 + +``` +=== SRDB Struct Tags Example === + +1. 从结构体生成 Schema +Schema 名称: users +字段数量: 8 + +字段详情: + - username: Type=string, Indexed=true, Nullable=false, Comment="用户名(索引)" + - age: Type=int64, Indexed=false, Nullable=false, Comment="年龄" + - email: Type=string, Indexed=false, Nullable=true, Comment="邮箱(可选)" + - phone_number: Type=string, Indexed=true, Nullable=true, Comment="手机号(可空且索引)" + - bio: Type=string, Indexed=false, Nullable=true, Comment="个人简介(可选)" + - avatar: Type=string, Indexed=false, Nullable=true, Comment="头像 URL(可选)" + - balance: Type=decimal, Indexed=false, Nullable=true, Comment="账户余额(可空)" + - is_active: Type=bool, Indexed=false, Nullable=false, Comment="是否激活" + +2. 创建数据库和表 +✓ 表创建成功 + +3. 插入完整数据 +✓ 插入用户 alice(完整数据) + +4. 插入部分数据(可选字段为 NULL) +✓ 插入用户 bob(email、bio、balance 为 NULL) + +5. 测试必填字段不能为 NULL +✓ 符合预期的错误: field username: NULL value not allowed (field is not nullable) + +6. 查询所有用户 + 用户: alice, 邮箱: alice@example.com, 余额: 1000.5 + 用户: bob, 邮箱: , 余额: + +7. 按索引字段查询(username='alice') + 找到用户: alice, 年龄: 25 + +✅ 所有操作完成! +``` + +## 自动字段名转换 + +如果不指定字段名,会自动将结构体字段名转换为 snake_case: + +```go +type User struct { + UserName string // -> user_name + EmailAddress string // -> email_address + IsActive bool // -> is_active + HTTPServer string // -> http_server +} +``` + +## Nullable 支持说明 + +1. **必填字段**(默认):不能插入 NULL 值,会返回错误 +2. **可选字段**(nullable=true):可以插入 NULL 值 +3. **查询结果**:NULL 值会以 `nil` 形式返回 +4. **验证时机**:在 `Insert()` 时自动验证 + +## 最佳实践 + +1. **对可选字段使用 nullable** + ```go + Email string `srdb:"email;nullable"` // ✓ 推荐 + Email string `srdb:"email"` // ✗ 如果是可选的,应该标记 nullable + ``` + +2. **对查询频繁的字段建立索引** + ```go + Username string `srdb:"username;indexed"` // ✓ 查询键 + Bio string `srdb:"bio"` // ✓ 不常查询的字段 + ``` + +3. **组合使用 nullable 和 indexed** + ```go + Phone string `srdb:"phone;nullable;indexed"` // ✓ 可选但需要索引查询 + ``` + +4. **为可选字段标记 nullable** + ```go + Avatar string `srdb:"avatar;nullable"` // ✓ 值类型 + nullable + Balance decimal.Decimal `srdb:"balance;nullable"` // ✓ 所有类型都支持 nullable + ``` diff --git a/examples/struct_tags/data/database.meta b/examples/struct_tags/data/database.meta new file mode 100644 index 0000000..efd7ee3 --- /dev/null +++ b/examples/struct_tags/data/database.meta @@ -0,0 +1,10 @@ +{ + "version": 1, + "tables": [ + { + "name": "users", + "dir": "users", + "created_at": 1760032030 + } + ] +} \ No newline at end of file diff --git a/examples/struct_tags/data/users/CURRENT b/examples/struct_tags/data/users/CURRENT new file mode 100644 index 0000000..7ed683d --- /dev/null +++ b/examples/struct_tags/data/users/CURRENT @@ -0,0 +1 @@ +MANIFEST-000001 diff --git a/examples/struct_tags/data/users/MANIFEST-000001 b/examples/struct_tags/data/users/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..6c53b5f00063d812c2e28fabae4cf694cbcd4c87 GIT binary patch literal 79 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq` ../.. + +require ( + code.tczkiot.com/wlw/srdb v0.0.0-00010101000000-000000000000 + github.com/shopspring/decimal v1.4.0 +) + +require ( + github.com/edsrzf/mmap-go v1.1.0 // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect +) diff --git a/examples/struct_tags/go.sum b/examples/struct_tags/go.sum new file mode 100644 index 0000000..79975e3 --- /dev/null +++ b/examples/struct_tags/go.sum @@ -0,0 +1,6 @@ +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/examples/struct_tags/main.go b/examples/struct_tags/main.go new file mode 100644 index 0000000..96a906f --- /dev/null +++ b/examples/struct_tags/main.go @@ -0,0 +1,176 @@ +package main + +import ( + "fmt" + "log" + + "code.tczkiot.com/wlw/srdb" + "github.com/shopspring/decimal" +) + +// User 用户结构体,展示 struct tag 的完整使用 +type User struct { + // 基本字段(必填) + Username string `srdb:"username;indexed;comment:用户名(索引)"` + Age int64 `srdb:"age;comment:年龄"` + + // 可选字段(nullable) + Email string `srdb:"email;nullable;comment:邮箱(可选)"` + PhoneNumber string `srdb:"phone_number;nullable;indexed;comment:手机号(可空且索引)"` + Bio string `srdb:"bio;nullable;comment:个人简介(可选)"` + Avatar string `srdb:"avatar;nullable;comment:头像 URL(可选)"` + + // 财务字段 + Balance decimal.Decimal `srdb:"balance;nullable;comment:账户余额(可空)"` + + // 布尔字段 + IsActive bool `srdb:"is_active;comment:是否激活"` + + // 忽略字段 + internalData string `srdb:"-"` // 未导出字段会自动忽略 + TempData string `srdb:"-"` // 使用 "-" 显式忽略导出字段 +} + +func main() { + fmt.Println("=== SRDB Struct Tags Example ===\n") + + // 1. 从结构体生成 Schema + fmt.Println("1. 从结构体生成 Schema") + fields, err := srdb.StructToFields(User{}) + if err != nil { + log.Fatalf("StructToFields failed: %v", err) + } + + schema, err := srdb.NewSchema("users", fields) + if err != nil { + log.Fatalf("NewSchema failed: %v", err) + } + + fmt.Printf("Schema 名称: %s\n", schema.Name) + fmt.Printf("字段数量: %d\n\n", len(schema.Fields)) + + // 打印所有字段 + fmt.Println("字段详情:") + for _, field := range schema.Fields { + fmt.Printf(" - %s: Type=%s, Indexed=%v, Nullable=%v", + field.Name, field.Type, field.Indexed, field.Nullable) + if field.Comment != "" { + fmt.Printf(", Comment=%q", field.Comment) + } + fmt.Println() + } + fmt.Println() + + // 2. 创建数据库和表 + fmt.Println("2. 创建数据库和表") + db, err := srdb.Open("./data") + if err != nil { + log.Fatalf("Open database failed: %v", err) + } + defer db.Close() + + table, err := db.CreateTable("users", schema) + if err != nil { + log.Fatalf("CreateTable failed: %v", err) + } + fmt.Println("✓ 表创建成功\n") + + // 3. 插入数据 - 完整数据(所有字段都有值) + fmt.Println("3. 插入完整数据") + avatar1 := "https://example.com/avatar1.png" + err = table.Insert(map[string]any{ + "username": "alice", + "age": int64(25), + "email": "alice@example.com", + "phone_number": "13800138001", + "bio": "Software Engineer", + "avatar": avatar1, + "balance": decimal.NewFromFloat(1000.50), + "is_active": true, + }) + if err != nil { + log.Fatalf("Insert failed: %v", err) + } + fmt.Println("✓ 插入用户 alice(完整数据)") + + // 4. 插入数据 - 部分字段为 NULL + fmt.Println("\n4. 插入部分数据(可选字段为 NULL)") + err = table.Insert(map[string]any{ + "username": "bob", + "age": int64(30), + "email": nil, // NULL 值 + "bio": nil, // NULL 值 + "balance": nil, // NULL 值 + "is_active": true, + }) + if err != nil { + log.Fatalf("Insert failed: %v", err) + } + fmt.Println("✓ 插入用户 bob(email、bio、balance 为 NULL)") + + // 5. 插入数据 - 必填字段不能为 NULL + fmt.Println("\n5. 测试必填字段不能为 NULL") + err = table.Insert(map[string]any{ + "username": nil, // 尝试将必填字段设为 NULL + "age": int64(28), + "is_active": true, + }) + if err != nil { + fmt.Printf("✓ 符合预期的错误: %v\n", err) + } else { + log.Fatal("应该返回错误,但成功了!") + } + + // 6. 查询所有数据 + fmt.Println("\n6. 查询所有用户") + rows, err := table.Query().Rows() + if err != nil { + log.Fatalf("Query failed: %v", err) + } + defer rows.Close() + + for rows.Next() { + row := rows.Row() + data := row.Data() + + username := data["username"] + email := data["email"] + balance := data["balance"] + + fmt.Printf(" 用户: %v", username) + if email == nil { + fmt.Printf(", 邮箱: ") + } else { + fmt.Printf(", 邮箱: %v", email) + } + if balance == nil { + fmt.Printf(", 余额: ") + } else { + fmt.Printf(", 余额: %v", balance) + } + fmt.Println() + } + + // 7. 按索引字段查询 + fmt.Println("\n7. 按索引字段查询(username='alice')") + rows2, err := table.Query().Eq("username", "alice").Rows() + if err != nil { + log.Fatalf("Query failed: %v", err) + } + defer rows2.Close() + + if rows2.Next() { + row := rows2.Row() + data := row.Data() + fmt.Printf(" 找到用户: %v, 年龄: %v\n", data["username"], data["age"]) + } + + fmt.Println("\n✅ 所有操作完成!") + fmt.Println("\nStruct Tag 使用总结:") + fmt.Println(" - srdb:\"name\" # 指定字段名") + fmt.Println(" - srdb:\"name;indexed\" # 字段名 + 索引") + fmt.Println(" - srdb:\"name;nullable\" # 字段名 + 可空") + fmt.Println(" - srdb:\"name;comment:注释\" # 字段名 + 注释") + fmt.Println(" - srdb:\"name;indexed;nullable;comment:XX\" # 完整格式") + fmt.Println(" - srdb:\"-\" # 忽略字段") +} diff --git a/examples/time_duration/main.go b/examples/time_duration/main.go new file mode 100644 index 0000000..8b74e8f --- /dev/null +++ b/examples/time_duration/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "fmt" + "log" + "os" + "time" + + "code.tczkiot.com/wlw/srdb" +) + +func main() { + fmt.Println("=== Testing Time and Duration Types ===\n") + + // 1. 创建 Schema + schema, err := srdb.NewSchema("events", []srdb.Field{ + {Name: "name", Type: srdb.String, Comment: "事件名称"}, + {Name: "created_at", Type: srdb.Time, Comment: "创建时间"}, + {Name: "duration", Type: srdb.Duration, Comment: "持续时间"}, + {Name: "count", Type: srdb.Int64, Comment: "计数"}, + }) + if err != nil { + log.Fatalf("创建 Schema 失败: %v", err) + } + + fmt.Println("✓ Schema 创建成功") + fmt.Printf(" 字段数: %d\n", len(schema.Fields)) + for _, field := range schema.Fields { + fmt.Printf(" - %s: %s (%s)\n", field.Name, field.Type.String(), field.Comment) + } + fmt.Println() + + // 2. 创建数据库和表 + os.RemoveAll("./test_time_data") + db, err := srdb.Open("./test_time_data") + if err != nil { + log.Fatalf("打开数据库失败: %v", err) + } + defer func() { + db.Close() + os.RemoveAll("./test_time_data") + }() + + table, err := db.CreateTable("events", schema) + if err != nil { + log.Fatalf("创建表失败: %v", err) + } + + fmt.Println("✓ 表创建成功\n") + + // 3. 插入数据(使用原生类型) + now := time.Now() + duration := 2 * time.Hour + + err = table.Insert(map[string]any{ + "name": "event1", + "created_at": now, + "duration": duration, + "count": int64(100), + }) + if err != nil { + log.Fatalf("插入数据失败: %v", err) + } + + fmt.Println("✓ 插入数据成功(使用原生类型)") + fmt.Printf(" 时间: %v\n", now) + fmt.Printf(" 持续时间: %v\n", duration) + fmt.Println() + + // 4. 插入数据(使用字符串格式) + err = table.Insert(map[string]any{ + "name": "event2", + "created_at": now.Format(time.RFC3339), + "duration": "1h30m", + "count": int64(200), + }) + if err != nil { + log.Fatalf("插入数据失败(字符串格式): %v", err) + } + + fmt.Println("✓ 插入数据成功(使用字符串格式)") + fmt.Printf(" 时间字符串: %s\n", now.Format(time.RFC3339)) + fmt.Printf(" 持续时间字符串: 1h30m\n") + fmt.Println() + + // 5. 插入数据(使用 int64 格式) + err = table.Insert(map[string]any{ + "name": "event3", + "created_at": now.Unix(), + "duration": int64(45 * time.Minute), + "count": int64(300), + }) + if err != nil { + log.Fatalf("插入数据失败(int64 格式): %v", err) + } + + fmt.Println("✓ 插入数据成功(使用 int64 格式)") + fmt.Printf(" Unix 时间戳: %d\n", now.Unix()) + fmt.Printf(" 持续时间(纳秒): %d\n", int64(45*time.Minute)) + fmt.Println() + + // 6. 查询数据 + fmt.Println("6. 查询所有数据") + rows, err := table.Query().Rows() + if err != nil { + log.Fatalf("查询失败: %v", err) + } + defer rows.Close() + + count := 0 + for rows.Next() { + count++ + row := rows.Row() + data := row.Data() + + name := data["name"] + createdAt := data["created_at"] + dur := data["duration"] + cnt := data["count"] + + fmt.Printf(" [%d] 名称: %v\n", count, name) + + // 验证类型 + if t, ok := createdAt.(time.Time); ok { + fmt.Printf(" 时间: %v (类型: time.Time) ✓\n", t.Format(time.RFC3339)) + } else { + fmt.Printf(" 时间: %v (类型: %T) ✗\n", createdAt, createdAt) + } + + if d, ok := dur.(time.Duration); ok { + fmt.Printf(" 持续时间: %v (类型: time.Duration) ✓\n", d) + } else { + fmt.Printf(" 持续时间: %v (类型: %T) ✗\n", dur, dur) + } + + fmt.Printf(" 计数: %v\n\n", cnt) + } + + fmt.Printf("✅ 所有测试完成! 共查询 %d 条记录\n", count) +} diff --git a/examples/time_duration_simple/main.go b/examples/time_duration_simple/main.go new file mode 100644 index 0000000..046380d --- /dev/null +++ b/examples/time_duration_simple/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "log" + "os" + "time" + + "code.tczkiot.com/wlw/srdb" +) + +func main() { + fmt.Println("=== Testing Time and Duration Types (Simple) ===\n") + + // 1. 创建 Schema + schema, err := srdb.NewSchema("events", []srdb.Field{ + {Name: "name", Type: srdb.String, Comment: "事件名称"}, + {Name: "created_at", Type: srdb.Time, Comment: "创建时间"}, + {Name: "duration", Type: srdb.Duration, Comment: "持续时间"}, + }) + if err != nil { + log.Fatalf("创建 Schema 失败: %v", err) + } + + fmt.Println("✓ Schema 创建成功") + for _, field := range schema.Fields { + fmt.Printf(" - %s: %s\n", field.Name, field.Type.String()) + } + fmt.Println() + + // 2. 创建数据库和表 + os.RemoveAll("./test_data") + db, err := srdb.Open("./test_data") + if err != nil { + log.Fatalf("打开数据库失败: %v", err) + } + + table, err := db.CreateTable("events", schema) + if err != nil { + log.Fatalf("创建表失败: %v", err) + } + + fmt.Println("✓ 表创建成功\n") + + // 3. 插入数据 + now := time.Now() + duration := 2 * time.Hour + + err = table.Insert(map[string]any{ + "name": "event1", + "created_at": now, + "duration": duration, + }) + if err != nil { + log.Fatalf("插入数据失败: %v", err) + } + + fmt.Println("✓ 插入数据成功") + fmt.Printf(" 时间: %v\n", now.Format(time.RFC3339)) + fmt.Printf(" 持续时间: %v\n\n", duration) + + // 4. 立即查询(从 MemTable) + fmt.Println("4. 查询数据(从 MemTable)") + rows, err := table.Query().Rows() + if err != nil { + log.Fatalf("查询失败: %v", err) + } + defer rows.Close() + + success := true + for rows.Next() { + row := rows.Row() + data := row.Data() + + name := data["name"] + createdAt := data["created_at"] + dur := data["duration"] + + fmt.Printf(" 名称: %v\n", name) + + // 验证类型 + if t, ok := createdAt.(time.Time); ok { + fmt.Printf(" 时间: %v (类型: time.Time) ✓\n", t.Format(time.RFC3339)) + } else { + fmt.Printf(" 时间: %v (类型: %T) ✗ FAILED\n", createdAt, createdAt) + success = false + } + + if d, ok := dur.(time.Duration); ok { + fmt.Printf(" 持续时间: %v (类型: time.Duration) ✓\n", d) + } else { + fmt.Printf(" 持续时间: %v (类型: %T) ✗ FAILED\n", dur, dur) + success = false + } + } + + if success { + fmt.Println("\n✅ 测试通过! Time 和 Duration 类型正确保留") + } else { + fmt.Println("\n❌ 测试失败! 类型未正确保留") + os.Exit(1) + } + + // 快速退出,不等待清理 + os.Exit(0) +} diff --git a/examples/webui/commands/webui.go b/examples/webui/commands/webui.go index c6ec267..d904b26 100644 --- a/examples/webui/commands/webui.go +++ b/examples/webui/commands/webui.go @@ -24,20 +24,20 @@ func StartWebUI(dbPath string, addr string) { // 创建示例 Schema userSchema, err := srdb.NewSchema("users", []srdb.Field{ - {Name: "name", Type: srdb.FieldTypeString, Indexed: true, Comment: "User name"}, - {Name: "email", Type: srdb.FieldTypeString, Indexed: false, Comment: "Email address"}, - {Name: "age", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "Age"}, - {Name: "city", Type: srdb.FieldTypeString, Indexed: false, Comment: "City"}, + {Name: "name", Type: srdb.String, Indexed: true, Comment: "User name"}, + {Name: "email", Type: srdb.String, Indexed: false, Comment: "Email address"}, + {Name: "age", Type: srdb.Int64, Indexed: false, Comment: "Age"}, + {Name: "city", Type: srdb.String, Indexed: false, Comment: "City"}, }) if err != nil { log.Fatal(err) } productSchema, err := srdb.NewSchema("products", []srdb.Field{ - {Name: "product_name", Type: srdb.FieldTypeString, Indexed: true, Comment: "Product name"}, - {Name: "price", Type: srdb.FieldTypeFloat, Indexed: false, Comment: "Price"}, - {Name: "quantity", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "Quantity"}, - {Name: "category", Type: srdb.FieldTypeString, Indexed: false, Comment: "Category"}, + {Name: "product_name", Type: srdb.String, Indexed: true, Comment: "Product name"}, + {Name: "price", Type: srdb.Float64, Indexed: false, Comment: "Price"}, + {Name: "quantity", Type: srdb.Int64, Indexed: false, Comment: "Quantity"}, + {Name: "category", Type: srdb.String, Indexed: false, Comment: "Category"}, }) if err != nil { log.Fatal(err) @@ -140,10 +140,10 @@ func autoInsertData(db *srdb.Database) { if !hasLogs { logsSchema, err := srdb.NewSchema("logs", []srdb.Field{ - {Name: "group", Type: srdb.FieldTypeString, Indexed: true, Comment: "Log group (A-E)"}, - {Name: "timestamp", Type: srdb.FieldTypeString, Indexed: false, Comment: "Timestamp"}, - {Name: "data", Type: srdb.FieldTypeString, Indexed: false, Comment: "Random data"}, - {Name: "size_bytes", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "Data size in bytes"}, + {Name: "group", Type: srdb.String, Indexed: true, Comment: "Log group (A-E)"}, + {Name: "timestamp", Type: srdb.String, Indexed: false, Comment: "Timestamp"}, + {Name: "data", Type: srdb.String, Indexed: false, Comment: "Random data"}, + {Name: "size_bytes", Type: srdb.Int64, Indexed: false, Comment: "Data size in bytes"}, }) if err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index f24e12e..9e15c0d 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,7 @@ go 1.24.0 require github.com/edsrzf/mmap-go v1.1.0 -require golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect +require ( + github.com/shopspring/decimal v1.4.0 // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect +) diff --git a/go.sum b/go.sum index 6dfc93e..79975e3 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/index_btree_test.go b/index_btree_test.go index 0435b62..03dcacc 100644 --- a/index_btree_test.go +++ b/index_btree_test.go @@ -13,9 +13,9 @@ func TestIndexBTreeBasic(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "name", Type: FieldTypeString}, - {Name: "city", Type: FieldTypeString}, + {Name: "id", Type: Int64}, + {Name: "name", Type: String}, + {Name: "city", Type: String}, }) if err != nil { t.Fatal(err) @@ -130,8 +130,8 @@ func TestIndexBTreeLargeDataset(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "category", Type: FieldTypeString}, + {Name: "id", Type: Int64}, + {Name: "category", Type: String}, }) if err != nil { t.Fatal(err) @@ -218,8 +218,8 @@ func TestIndexBTreeBackwardCompatibility(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "status", Type: FieldTypeString}, + {Name: "id", Type: Int64}, + {Name: "status", Type: String}, }) if err != nil { t.Fatal(err) @@ -296,8 +296,8 @@ func TestIndexBTreeIncrementalUpdate(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "tag", Type: FieldTypeString}, + {Name: "id", Type: Int64}, + {Name: "tag", Type: String}, }) if err != nil { t.Fatal(err) diff --git a/index_test.go b/index_test.go index dbbe771..db42a9e 100644 --- a/index_test.go +++ b/index_test.go @@ -12,7 +12,7 @@ func TestIndexVersionControl(t *testing.T) { defer os.RemoveAll(dir) testSchema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, }) if err != nil { t.Fatal(err) @@ -76,7 +76,7 @@ func TestIncrementalUpdate(t *testing.T) { defer os.RemoveAll(dir) testSchema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, }) if err != nil { t.Fatal(err) @@ -150,7 +150,7 @@ func TestNeedsUpdate(t *testing.T) { defer os.RemoveAll(dir) testSchema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, }) if err != nil { t.Fatal(err) @@ -184,8 +184,8 @@ func TestIndexPersistence(t *testing.T) { // 创建 Schema testSchema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -266,7 +266,7 @@ func TestIndexDropWithFile(t *testing.T) { defer os.RemoveAll(dir) testSchema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, }) if err != nil { t.Fatal(err) @@ -306,9 +306,9 @@ func TestIndexQueryIntegration(t *testing.T) { // 1. 创建带索引字段的 Schema schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false}, - {Name: "email", Type: FieldTypeString, Indexed: true}, // email 字段有索引 - {Name: "age", Type: FieldTypeInt64, Indexed: false}, + {Name: "name", Type: String, Indexed: false}, + {Name: "email", Type: String, Indexed: true}, // email 字段有索引 + {Name: "age", Type: Int64, Indexed: false}, }) if err != nil { t.Fatal(err) @@ -469,9 +469,9 @@ func TestIndexPersistenceAcrossRestart(t *testing.T) { // 1. 第一次打开:创建数据和索引 { schema, err := NewSchema("products", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false}, - {Name: "category", Type: FieldTypeString, Indexed: true}, - {Name: "price", Type: FieldTypeInt64, Indexed: false}, + {Name: "name", Type: String, Indexed: false}, + {Name: "category", Type: String, Indexed: true}, + {Name: "price", Type: Int64, Indexed: false}, }) if err != nil { t.Fatal(err) diff --git a/query_lazy_test.go b/query_lazy_test.go index f07838a..a4f726b 100644 --- a/query_lazy_test.go +++ b/query_lazy_test.go @@ -12,8 +12,8 @@ func TestLazyLoadingBasic(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -71,8 +71,8 @@ func TestLazyLoadingVsEagerLoading(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -151,9 +151,9 @@ func TestIndexQueryIsEager(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "email", Type: FieldTypeString, Indexed: true}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "email", Type: String, Indexed: true}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -236,9 +236,9 @@ func TestLazyLoadingWithConditions(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, - {Name: "active", Type: FieldTypeBool}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, + {Name: "active", Type: Bool}, }) if err != nil { t.Fatal(err) @@ -314,8 +314,8 @@ func TestFirstDoesNotLoadAll(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) diff --git a/schema.go b/schema.go index cc840e0..267e204 100644 --- a/schema.go +++ b/schema.go @@ -8,28 +8,92 @@ import ( "sort" "strings" "time" + + "github.com/shopspring/decimal" ) -// FieldType 字段类型 +// FieldType 字段类型(对应 Go 基础类型) type FieldType int const ( - FieldTypeInt64 FieldType = 1 - FieldTypeString FieldType = 2 - FieldTypeFloat FieldType = 3 - FieldTypeBool FieldType = 4 + _ FieldType = iota + + // 有符号整数类型 + Int + Int8 + Int16 + Int32 + Int64 + + // 无符号整数类型 + Uint + Uint8 + Uint16 + Uint32 + Uint64 + + // 浮点类型 + Float32 + Float64 + + // 字符串类型 + String + + // 布尔类型 + Bool + + // Byte 和 Rune 类型(独立类型,语义上对应 Go 的 byte 和 rune) + Byte // byte 类型(底层为 uint8) + Rune // rune 类型(底层为 int32) + + // Decimal 类型(高精度十进制,用于金融计算) + Decimal + + // 时间类型 + Time // time.Time 时间戳 + Duration // time.Duration 时间间隔 ) func (t FieldType) String() string { switch t { - case FieldTypeInt64: + case Int: + return "int" + case Int8: + return "int8" + case Int16: + return "int16" + case Int32: + return "int32" + case Int64: return "int64" - case FieldTypeString: - return "string" - case FieldTypeFloat: + case Uint: + return "uint" + case Uint8: + return "uint8" + case Uint16: + return "uint16" + case Uint32: + return "uint32" + case Uint64: + return "uint64" + case Float32: + return "float32" + case Float64: return "float64" - case FieldTypeBool: + case String: + return "string" + case Bool: return "bool" + case Byte: + return "byte" + case Rune: + return "rune" + case Decimal: + return "decimal" + case Time: + return "time" + case Duration: + return "duration" default: return "unknown" } @@ -37,10 +101,11 @@ func (t FieldType) String() string { // Field 字段定义 type Field struct { - Name string // 字段名 - Type FieldType // 字段类型 - Indexed bool // 是否建立索引 - Comment string // 注释 + Name string // 字段名 + Type FieldType // 字段类型 + Indexed bool // 是否建立索引 + Nullable bool // 是否允许 NULL 值 + Comment string // 注释 } // Schema 表结构定义 @@ -91,13 +156,15 @@ func NewSchema(name string, fields []Field) (*Schema, error) { // 支持的 struct tag 格式: // - `srdb:"name"` - 指定字段名(默认使用 snake_case 转换) // - `srdb:"name;indexed"` - 指定字段名并标记为索引 -// - `srdb:"name;indexed;comment:用户名"` - 完整格式(字段名;索引标记;注释) +// - `srdb:"name;nullable"` - 指定字段名并标记为可空 +// - `srdb:"name;indexed;nullable;comment:用户名"` - 完整格式 // - `srdb:"-"` - 忽略该字段 // // Tag 格式说明: // - 使用分号 `;` 分隔不同的部分 // - 第一部分是字段名(可选,默认使用 snake_case 转换结构体字段名) // - `indexed` 标记该字段需要索引 +// - `nullable` 标记该字段允许 NULL 值 // - `comment:注释内容` 指定字段注释 // // 默认字段名转换示例: @@ -105,19 +172,32 @@ func NewSchema(name string, fields []Field) (*Schema, error) { // - EmailAddress -> email_address // - IsActive -> is_active // -// 类型映射: -// - int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8 -> FieldTypeInt64 -// - string -> FieldTypeString -// - float64, float32 -> FieldTypeFloat -// - bool -> FieldTypeBool +// 类型映射(精确映射到 Go 基础类型): +// - int -> FieldTypeInt +// - int8 -> Int8 +// - int16 -> Int16 +// - int32 -> Int32 +// - int64 -> Int64 +// - uint -> Uint +// - uint8 (byte) -> Uint8 或 Byte +// - uint16 -> Uint16 +// - uint32 -> Uint32 +// - uint64 -> Uint64 +// - float32 -> Float32 +// - float64 -> Float64 +// - string -> String +// - bool -> Bool +// - rune -> Rune +// - decimal.Decimal -> Decimal // // 示例: -// type User struct { -// Name string `srdb:"name;indexed;comment:用户名"` -// Age int64 `srdb:"age;comment:年龄"` -// Email string `srdb:"email;indexed;comment:邮箱"` -// } -// fields, err := StructToFields(User{}) +// +// type User struct { +// Name string `srdb:"name;indexed;comment:用户名"` +// Age int64 `srdb:"age;comment:年龄"` +// Email *string `srdb:"email;nullable;comment:邮箱(可选)"` +// } +// fields, err := StructToFields(User{}) // // 参数: // - v: 结构体实例或指针 @@ -133,7 +213,7 @@ func StructToFields(v any) ([]Field, error) { } // 如果是指针,获取其指向的类型 - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { typ = typ.Elem() } @@ -160,9 +240,10 @@ func StructToFields(v any) ([]Field, error) { continue } - // 解析字段名、索引标记和注释 + // 解析字段名、索引标记、nullable 和注释 fieldName := camelToSnake(field.Name) // 默认使用 snake_case 字段名 indexed := false + nullable := false comment := "" if tag != "" { @@ -178,9 +259,12 @@ func StructToFields(v any) ([]Field, error) { } else if part == "indexed" { // indexed 标记 indexed = true - } else if strings.HasPrefix(part, "comment:") { + } else if part == "nullable" { + // nullable 标记 + nullable = true + } else if after, ok := strings.CutPrefix(part, "comment:"); ok { // comment:注释内容 - comment = strings.TrimPrefix(part, "comment:") + comment = after } } } @@ -192,10 +276,11 @@ func StructToFields(v any) ([]Field, error) { } fields = append(fields, Field{ - Name: fieldName, - Type: fieldType, - Indexed: indexed, - Comment: comment, + Name: fieldName, + Type: fieldType, + Indexed: indexed, + Nullable: nullable, + Comment: comment, }) } @@ -206,18 +291,64 @@ func StructToFields(v any) ([]Field, error) { return fields, nil } -// goTypeToFieldType 将 Go 类型映射到 FieldType +// goTypeToFieldType 将 Go 类型精确映射到 FieldType func goTypeToFieldType(typ reflect.Type) (FieldType, error) { + // 特殊处理:decimal.Decimal + if typ.PkgPath() == "github.com/shopspring/decimal" && typ.Name() == "Decimal" { + return Decimal, nil + } + + // 特殊处理:time.Time + if typ.PkgPath() == "time" && typ.Name() == "Time" { + return Time, nil + } + + // 特殊处理:time.Duration + if typ.PkgPath() == "time" && typ.Name() == "Duration" { + return Duration, nil + } + switch typ.Kind() { - case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, - reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: - return FieldTypeInt64, nil + case reflect.Int: + return Int, nil + case reflect.Int8: + // byte 在 Go 中是 uint8 的别名,但在反射中无法区分 + // 所以 int8 总是映射到 Int8 + return Int8, nil + case reflect.Int16: + return Int16, nil + case reflect.Int32: + // rune 在 Go 中是 int32 的别名 + // 如果 type 名称是 "rune",则映射到 Rune + if typ.Name() == "rune" { + return Rune, nil + } + return Int32, nil + case reflect.Int64: + return Int64, nil + case reflect.Uint: + return Uint, nil + case reflect.Uint8: + // byte 是 uint8 的别名 + // 如果 type 名称是 "byte",则映射到 Byte + if typ.Name() == "byte" { + return Byte, nil + } + return Uint8, nil + case reflect.Uint16: + return Uint16, nil + case reflect.Uint32: + return Uint32, nil + case reflect.Uint64: + return Uint64, nil + case reflect.Float32: + return Float32, nil + case reflect.Float64: + return Float64, nil case reflect.String: - return FieldTypeString, nil - case reflect.Float64, reflect.Float32: - return FieldTypeFloat, nil + return String, nil case reflect.Bool: - return FieldTypeBool, nil + return Bool, nil default: return 0, fmt.Errorf("unsupported type: %s", typ.Kind()) } @@ -308,6 +439,15 @@ func (s *Schema) Validate(data map[string]any) error { continue } + // 检查 NULL 值 + if value == nil { + if !field.Nullable { + return fmt.Errorf("field %s: NULL value not allowed (field is not nullable)", field.Name) + } + // NULL 值且字段允许 NULL,跳过类型验证 + continue + } + // 验证类型 if err := s.validateType(field.Type, value); err != nil { return fmt.Errorf("field %s: %v", field.Name, err) @@ -324,36 +464,193 @@ func (s *Schema) ValidateType(typ FieldType, value any) error { // validateType 验证值的类型 func (s *Schema) validateType(typ FieldType, value any) error { switch typ { - case FieldTypeInt64: - switch value.(type) { - case int, int64, int32, int16, int8: + // 有符号整数类型 + case Int, Int8, Int16, Int32, Int64: + switch v := value.(type) { + case int, int8, int16, int32, int64: + return nil + case uint, uint8, uint16, uint32, uint64: + return nil // 允许无符号整数,稍后转换 + case float64: + // JSON 解析后数字都是 float64,检查是否为整数 + if v == float64(int64(v)) { + return nil + } + return fmt.Errorf("expected integer, got float %v", v) + case float32: + if v == float32(int32(v)) { + return nil + } + return fmt.Errorf("expected integer, got float %v", v) + default: + return fmt.Errorf("expected integer type (%s), got %T", typ.String(), value) + } + + // 无符号整数类型 + case Uint, Uint8, Uint16, Uint32, Uint64: + switch v := value.(type) { + case uint, uint8, uint16, uint32, uint64: + return nil + case int, int8, int16, int32, int64: + // 允许有符号整数,但必须非负 + if reflect.ValueOf(v).Int() < 0 { + return fmt.Errorf("expected non-negative integer for %s, got %v", typ.String(), v) + } return nil case float64: - // JSON 解析后数字都是 float64 + if v < 0 || v != float64(uint64(v)) { + return fmt.Errorf("expected non-negative integer for %s, got %v", typ.String(), v) + } + return nil + case float32: + if v < 0 || v != float32(uint32(v)) { + return fmt.Errorf("expected non-negative integer for %s, got %v", typ.String(), v) + } return nil default: - return fmt.Errorf("expected int64, got %T", value) + return fmt.Errorf("expected unsigned integer type (%s), got %T", typ.String(), value) } - case FieldTypeString: + + // Byte 类型(底层为 uint8) + case Byte: + switch v := value.(type) { + case uint8: // byte 和 uint8 是同一类型,只需一个 case + return nil + case int, int8, int16, int32, int64: + if reflect.ValueOf(v).Int() < 0 || reflect.ValueOf(v).Int() > 255 { + return fmt.Errorf("expected byte value (0-255), got %v", v) + } + return nil + case uint, uint16, uint32, uint64: + if reflect.ValueOf(v).Uint() > 255 { + return fmt.Errorf("expected byte value (0-255), got %v", v) + } + return nil + case float64: + if v < 0 || v > 255 || v != float64(uint8(v)) { + return fmt.Errorf("expected byte value (0-255), got %v", v) + } + return nil + default: + return fmt.Errorf("expected byte type, got %T", value) + } + + // Rune 类型(底层为 int32) + case Rune: + switch v := value.(type) { + case int32: // rune 和 int32 是同一类型,只需一个 case + return nil + case int, int8, int16, int64: + return nil + case uint, uint8, uint16, uint32, uint64: + return nil + case float64: + if v != float64(int32(v)) { + return fmt.Errorf("expected rune (int32), got float %v", v) + } + return nil + case string: + // 允许单字符字符串转换为 rune + if len([]rune(v)) == 1 { + return nil + } + return fmt.Errorf("expected single character string for rune, got %q", v) + default: + return fmt.Errorf("expected rune type, got %T", value) + } + + // 浮点类型 + case Float32, Float64: + switch value.(type) { + case float32, float64: + return nil + case int, int8, int16, int32, int64: + return nil // 整数可以转换为浮点数 + case uint, uint8, uint16, uint32, uint64: + return nil + default: + return fmt.Errorf("expected float type (%s), got %T", typ.String(), value) + } + + // Decimal 类型 + case Decimal: + switch v := value.(type) { + case decimal.Decimal: + return nil + case string: + // 允许字符串转换为 Decimal + _, err := decimal.NewFromString(v) + if err != nil { + return fmt.Errorf("expected decimal value, got invalid string %q: %v", v, err) + } + return nil + case float32, float64: + return nil // 浮点数可以转换为 Decimal + case int, int8, int16, int32, int64: + return nil + case uint, uint8, uint16, uint32, uint64: + return nil + default: + return fmt.Errorf("expected decimal type, got %T", value) + } + + // 字符串类型 + case String: if _, ok := value.(string); !ok { return fmt.Errorf("expected string, got %T", value) } - case FieldTypeFloat: - switch value.(type) { - case float64, float32: - return nil - default: - return fmt.Errorf("expected float, got %T", value) - } - case FieldTypeBool: + + // 布尔类型 + case Bool: if _, ok := value.(bool); !ok { return fmt.Errorf("expected bool, got %T", value) } + + // 时间类型 + case Time: + switch v := value.(type) { + case time.Time: + return nil + case string: + // 允许字符串转换为 Time (RFC3339 格式) + _, err := time.Parse(time.RFC3339, v) + if err != nil { + return fmt.Errorf("expected time value, got invalid string %q: %v", v, err) + } + return nil + case int64: + // 允许 Unix 时间戳(秒) + return nil + default: + return fmt.Errorf("expected time type, got %T", value) + } + + // 时间间隔类型 + case Duration: + switch v := value.(type) { + case time.Duration: + return nil + case int64: + // 允许 int64 (纳秒) + return nil + case string: + // 允许字符串转换为 Duration (如 "1h30m") + _, err := time.ParseDuration(v) + if err != nil { + return fmt.Errorf("expected duration value, got invalid string %q: %v", v, err) + } + return nil + default: + return fmt.Errorf("expected duration type, got %T", value) + } + + default: + return fmt.Errorf("unknown field type: %v", typ) } return nil } -// ExtractIndexValue 提取索引值 +// ExtractIndexValue 提取索引值(支持类型转换) func (s *Schema) ExtractIndexValue(field string, data map[string]any) (any, error) { fieldDef, err := s.GetField(field) if err != nil { @@ -365,37 +662,511 @@ func (s *Schema) ExtractIndexValue(field string, data map[string]any) (any, erro return nil, NewErrorf(ErrCodeFieldNotFound, "field %s not found in data", field) } - // 类型转换 - switch fieldDef.Type { - case FieldTypeInt64: - switch v := value.(type) { - case int: - return int64(v), nil - case int64: - return v, nil - case float64: - return int64(v), nil - default: - return nil, fmt.Errorf("cannot convert %T to int64", value) - } - case FieldTypeString: + return convertValue(value, fieldDef.Type) +} + +// convertValue 将值转换为目标类型 +func convertValue(value any, targetType FieldType) (any, error) { + switch targetType { + // 有符号整数类型 + case Int: + return convertToInt(value) + case Int8: + return convertToInt8(value) + case Int16: + return convertToInt16(value) + case Int32: + return convertToInt32(value) + case Int64: + return convertToInt64(value) + + // 无符号整数类型 + case Uint: + return convertToUint(value) + case Uint8: + return convertToUint8(value) + case Uint16: + return convertToUint16(value) + case Uint32: + return convertToUint32(value) + case Uint64: + return convertToUint64(value) + + // Byte 和 Rune 类型 + case Byte: + return convertToByte(value) + case Rune: + return convertToRune(value) + + // 浮点类型 + case Float32: + return convertToFloat32(value) + case Float64: + return convertToFloat64(value) + + // Decimal 类型 + case Decimal: + return convertToDecimal(value) + + // 字符串类型 + case String: if v, ok := value.(string); ok { return v, nil } return nil, fmt.Errorf("cannot convert %T to string", value) - case FieldTypeFloat: - if v, ok := value.(float64); ok { - return v, nil - } - return nil, fmt.Errorf("cannot convert %T to float64", value) - case FieldTypeBool: + + // 布尔类型 + case Bool: if v, ok := value.(bool); ok { return v, nil } return nil, fmt.Errorf("cannot convert %T to bool", value) - } - return nil, fmt.Errorf("unsupported type: %v", fieldDef.Type) + // 时间类型 + case Time: + return convertToTime(value) + + // 时间间隔类型 + case Duration: + return convertToDuration(value) + + default: + return nil, fmt.Errorf("unsupported type: %v", targetType) + } +} + +// 类型转换辅助函数 +func convertToInt(v any) (int, error) { + switch val := v.(type) { + case int: + return val, nil + case int8: + return int(val), nil + case int16: + return int(val), nil + case int32: + return int(val), nil + case int64: + return int(val), nil + case uint: + return int(val), nil + case uint8: + return int(val), nil + case uint16: + return int(val), nil + case uint32: + return int(val), nil + case uint64: + return int(val), nil + case float32: + return int(val), nil + case float64: + return int(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to int", v) + } +} + +func convertToInt8(v any) (int8, error) { + switch val := v.(type) { + case int: + return int8(val), nil + case int8: + return val, nil + case int16: + return int8(val), nil + case int32: + return int8(val), nil + case int64: + return int8(val), nil + case uint: + return int8(val), nil + case uint8: + return int8(val), nil + case uint16: + return int8(val), nil + case uint32: + return int8(val), nil + case uint64: + return int8(val), nil + case float32: + return int8(val), nil + case float64: + return int8(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to int8", v) + } +} + +func convertToInt16(v any) (int16, error) { + switch val := v.(type) { + case int: + return int16(val), nil + case int8: + return int16(val), nil + case int16: + return val, nil + case int32: + return int16(val), nil + case int64: + return int16(val), nil + case uint: + return int16(val), nil + case uint8: + return int16(val), nil + case uint16: + return int16(val), nil + case uint32: + return int16(val), nil + case uint64: + return int16(val), nil + case float32: + return int16(val), nil + case float64: + return int16(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to int16", v) + } +} + +func convertToInt32(v any) (int32, error) { + switch val := v.(type) { + case int: + return int32(val), nil + case int8: + return int32(val), nil + case int16: + return int32(val), nil + case int32: + return val, nil + case int64: + return int32(val), nil + case uint: + return int32(val), nil + case uint8: + return int32(val), nil + case uint16: + return int32(val), nil + case uint32: + return int32(val), nil + case uint64: + return int32(val), nil + case float32: + return int32(val), nil + case float64: + return int32(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to int32", v) + } +} + +func convertToInt64(v any) (int64, error) { + switch val := v.(type) { + case int: + return int64(val), nil + case int8: + return int64(val), nil + case int16: + return int64(val), nil + case int32: + return int64(val), nil + case int64: + return val, nil + case uint: + return int64(val), nil + case uint8: + return int64(val), nil + case uint16: + return int64(val), nil + case uint32: + return int64(val), nil + case uint64: + return int64(val), nil + case float32: + return int64(val), nil + case float64: + return int64(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to int64", v) + } +} + +func convertToUint(v any) (uint, error) { + switch val := v.(type) { + case uint: + return val, nil + case uint8: + return uint(val), nil + case uint16: + return uint(val), nil + case uint32: + return uint(val), nil + case uint64: + return uint(val), nil + case int: + if val < 0 { + return 0, fmt.Errorf("cannot convert negative int %d to uint", val) + } + return uint(val), nil + case int8: + if val < 0 { + return 0, fmt.Errorf("cannot convert negative int8 %d to uint", val) + } + return uint(val), nil + case int16: + if val < 0 { + return 0, fmt.Errorf("cannot convert negative int16 %d to uint", val) + } + return uint(val), nil + case int32: + if val < 0 { + return 0, fmt.Errorf("cannot convert negative int32 %d to uint", val) + } + return uint(val), nil + case int64: + if val < 0 { + return 0, fmt.Errorf("cannot convert negative int64 %d to uint", val) + } + return uint(val), nil + case float32: + if val < 0 { + return 0, fmt.Errorf("cannot convert negative float32 %v to uint", val) + } + return uint(val), nil + case float64: + if val < 0 { + return 0, fmt.Errorf("cannot convert negative float64 %v to uint", val) + } + return uint(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to uint", v) + } +} + +func convertToUint8(v any) (uint8, error) { + val, err := convertToUint(v) + if err != nil { + return 0, err + } + return uint8(val), nil +} + +func convertToUint16(v any) (uint16, error) { + val, err := convertToUint(v) + if err != nil { + return 0, err + } + return uint16(val), nil +} + +func convertToUint32(v any) (uint32, error) { + val, err := convertToUint(v) + if err != nil { + return 0, err + } + return uint32(val), nil +} + +func convertToUint64(v any) (uint64, error) { + val, err := convertToUint(v) + if err != nil { + return 0, err + } + return uint64(val), nil +} + +func convertToFloat32(v any) (float32, error) { + switch val := v.(type) { + case float32: + return val, nil + case float64: + return float32(val), nil + case int: + return float32(val), nil + case int8: + return float32(val), nil + case int16: + return float32(val), nil + case int32: + return float32(val), nil + case int64: + return float32(val), nil + case uint: + return float32(val), nil + case uint8: + return float32(val), nil + case uint16: + return float32(val), nil + case uint32: + return float32(val), nil + case uint64: + return float32(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to float32", v) + } +} + +func convertToFloat64(v any) (float64, error) { + switch val := v.(type) { + case float64: + return val, nil + case float32: + return float64(val), nil + case int: + return float64(val), nil + case int8: + return float64(val), nil + case int16: + return float64(val), nil + case int32: + return float64(val), nil + case int64: + return float64(val), nil + case uint: + return float64(val), nil + case uint8: + return float64(val), nil + case uint16: + return float64(val), nil + case uint32: + return float64(val), nil + case uint64: + return float64(val), nil + default: + return 0, fmt.Errorf("cannot convert %T to float64", v) + } +} + +// convertToByte 将值转换为 byte (uint8) +func convertToByte(v any) (byte, error) { + switch val := v.(type) { + case uint8: // byte 和 uint8 是同一类型 + return val, nil + case uint, uint16, uint32, uint64: + uval := reflect.ValueOf(val).Uint() + if uval > 255 { + return 0, fmt.Errorf("value %d out of byte range (0-255)", uval) + } + return byte(uval), nil + case int, int8, int16, int32, int64: + ival := reflect.ValueOf(val).Int() + if ival < 0 || ival > 255 { + return 0, fmt.Errorf("value %d out of byte range (0-255)", ival) + } + return byte(ival), nil + case float32, float64: + fval := reflect.ValueOf(val).Float() + if fval < 0 || fval > 255 { + return 0, fmt.Errorf("value %f out of byte range (0-255)", fval) + } + return byte(fval), nil + default: + return 0, fmt.Errorf("cannot convert %T to byte", v) + } +} + +// convertToRune 将值转换为 rune (int32) +func convertToRune(v any) (rune, error) { + switch val := v.(type) { + case int32: // rune 和 int32 是同一类型 + return val, nil + case int, int8, int16, int64: + return rune(reflect.ValueOf(val).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return rune(reflect.ValueOf(val).Uint()), nil + case float32, float64: + return rune(reflect.ValueOf(val).Float()), nil + case string: + // 单字符字符串转换为 rune + runes := []rune(val) + if len(runes) == 1 { + return runes[0], nil + } + return 0, fmt.Errorf("cannot convert multi-character string %q to rune", val) + default: + return 0, fmt.Errorf("cannot convert %T to rune", v) + } +} + +// convertToDecimal 将值转换为 decimal.Decimal +func convertToDecimal(v any) (decimal.Decimal, error) { + switch val := v.(type) { + case decimal.Decimal: + return val, nil + case string: + d, err := decimal.NewFromString(val) + if err != nil { + return decimal.Decimal{}, fmt.Errorf("invalid decimal string %q: %w", val, err) + } + return d, nil + case float32: + return decimal.NewFromFloat32(val), nil + case float64: + return decimal.NewFromFloat(val), nil + case int: + return decimal.NewFromInt(int64(val)), nil + case int8: + return decimal.NewFromInt(int64(val)), nil + case int16: + return decimal.NewFromInt(int64(val)), nil + case int32: + return decimal.NewFromInt32(val), nil + case int64: + return decimal.NewFromInt(val), nil + case uint: + return decimal.NewFromInt(int64(val)), nil + case uint8: + return decimal.NewFromInt(int64(val)), nil + case uint16: + return decimal.NewFromInt(int64(val)), nil + case uint32: + return decimal.NewFromInt(int64(val)), nil + case uint64: + // uint64 可能超出 int64 范围,使用字符串转换 + return decimal.NewFromString(fmt.Sprintf("%d", val)) + default: + return decimal.Decimal{}, fmt.Errorf("cannot convert %T to decimal", v) + } +} + +// convertToTime 将值转换为 time.Time +func convertToTime(v any) (time.Time, error) { + switch val := v.(type) { + case time.Time: + return val, nil + case string: + // 尝试 RFC3339 格式 + t, err := time.Parse(time.RFC3339, val) + if err != nil { + return time.Time{}, fmt.Errorf("invalid time string %q: %w", val, err) + } + return t, nil + case int64: + // Unix 时间戳(秒) + return time.Unix(val, 0), nil + default: + return time.Time{}, fmt.Errorf("cannot convert %T to time", v) + } +} + +// convertToDuration 将值转换为 time.Duration +func convertToDuration(v any) (time.Duration, error) { + switch val := v.(type) { + case time.Duration: + return val, nil + case int64: + // 纳秒 + return time.Duration(val), nil + case string: + // 字符串格式 (如 "1h30m") + d, err := time.ParseDuration(val) + if err != nil { + return 0, fmt.Errorf("invalid duration string %q: %w", val, err) + } + return d, nil + default: + return 0, fmt.Errorf("cannot convert %T to duration", v) + } } // ComputeChecksum 计算 Schema 的 SHA256 校验和 diff --git a/schema_test.go b/schema_test.go index 252db9d..3702de5 100644 --- a/schema_test.go +++ b/schema_test.go @@ -17,10 +17,10 @@ func init() { // UserSchema 用户表 Schema UserSchema, err = NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, - {Name: "email", Type: FieldTypeString, Indexed: true, Comment: "邮箱"}, - {Name: "description", Type: FieldTypeString, Indexed: false, Comment: "描述"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, + {Name: "email", Type: String, Indexed: true, Comment: "邮箱"}, + {Name: "description", Type: String, Indexed: false, Comment: "描述"}, }) if err != nil { panic("Failed to create UserSchema: " + err.Error()) @@ -28,10 +28,10 @@ func init() { // LogSchema 日志表 Schema LogSchema, err = NewSchema("logs", []Field{ - {Name: "level", Type: FieldTypeString, Indexed: true, Comment: "日志级别"}, - {Name: "message", Type: FieldTypeString, Indexed: false, Comment: "日志消息"}, - {Name: "source", Type: FieldTypeString, Indexed: true, Comment: "来源"}, - {Name: "error_code", Type: FieldTypeInt64, Indexed: true, Comment: "错误码"}, + {Name: "level", Type: String, Indexed: true, Comment: "日志级别"}, + {Name: "message", Type: String, Indexed: false, Comment: "日志消息"}, + {Name: "source", Type: String, Indexed: true, Comment: "来源"}, + {Name: "error_code", Type: Int64, Indexed: true, Comment: "错误码"}, }) if err != nil { panic("Failed to create LogSchema: " + err.Error()) @@ -39,11 +39,11 @@ func init() { // OrderSchema 订单表 Schema OrderSchema, err = NewSchema("orders", []Field{ - {Name: "order_id", Type: FieldTypeString, Indexed: true, Comment: "订单ID"}, - {Name: "user_id", Type: FieldTypeInt64, Indexed: true, Comment: "用户ID"}, - {Name: "amount", Type: FieldTypeFloat, Indexed: true, Comment: "金额"}, - {Name: "status", Type: FieldTypeString, Indexed: true, Comment: "状态"}, - {Name: "paid", Type: FieldTypeBool, Indexed: true, Comment: "是否支付"}, + {Name: "order_id", Type: String, Indexed: true, Comment: "订单ID"}, + {Name: "user_id", Type: Int64, Indexed: true, Comment: "用户ID"}, + {Name: "amount", Type: Float64, Indexed: true, Comment: "金额"}, + {Name: "status", Type: String, Indexed: true, Comment: "状态"}, + {Name: "paid", Type: Bool, Indexed: true, Comment: "是否支付"}, }) if err != nil { panic("Failed to create OrderSchema: " + err.Error()) @@ -53,9 +53,9 @@ func init() { func TestSchema(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, - {Name: "score", Type: FieldTypeFloat, Indexed: false, Comment: "分数"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, + {Name: "score", Type: Float64, Indexed: false, Comment: "分数"}, }) if err != nil { t.Fatal(err) @@ -85,8 +85,8 @@ func TestSchema(t *testing.T) { func TestSchemaValidation(t *testing.T) { schema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -130,15 +130,15 @@ func TestNewSchemaValidation(t *testing.T) { name: "Valid schema", schemaName: "users", fields: []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "name", Type: FieldTypeString}, + {Name: "id", Type: Int64}, + {Name: "name", Type: String}, }, shouldError: false, }, { name: "Empty schema name", schemaName: "", - fields: []Field{{Name: "id", Type: FieldTypeInt64}}, + fields: []Field{{Name: "id", Type: Int64}}, shouldError: true, errorMsg: "schema name cannot be empty", }, @@ -160,7 +160,7 @@ func TestNewSchemaValidation(t *testing.T) { name: "Empty field name at index 0", schemaName: "users", fields: []Field{ - {Name: "", Type: FieldTypeInt64}, + {Name: "", Type: Int64}, }, shouldError: true, errorMsg: "field at index 0 has empty name", @@ -169,8 +169,8 @@ func TestNewSchemaValidation(t *testing.T) { name: "Empty field name at index 1", schemaName: "users", fields: []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "", Type: FieldTypeString}, + {Name: "id", Type: Int64}, + {Name: "", Type: String}, }, shouldError: true, errorMsg: "field at index 1 has empty name", @@ -179,9 +179,9 @@ func TestNewSchemaValidation(t *testing.T) { name: "Duplicate field name", schemaName: "users", fields: []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "name", Type: FieldTypeString}, - {Name: "id", Type: FieldTypeString}, // Duplicate + {Name: "id", Type: Int64}, + {Name: "name", Type: String}, + {Name: "id", Type: String}, // Duplicate }, shouldError: true, errorMsg: "duplicate field name: id", @@ -190,7 +190,7 @@ func TestNewSchemaValidation(t *testing.T) { name: "Valid schema with single field", schemaName: "logs", fields: []Field{ - {Name: "message", Type: FieldTypeString}, + {Name: "message", Type: String}, }, shouldError: false, }, @@ -198,9 +198,9 @@ func TestNewSchemaValidation(t *testing.T) { name: "Valid schema with indexed field", schemaName: "users", fields: []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: true}, - {Name: "email", Type: FieldTypeString, Indexed: true}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "id", Type: Int64, Indexed: true}, + {Name: "email", Type: String, Indexed: true}, + {Name: "age", Type: Int64}, }, shouldError: false, }, @@ -208,9 +208,9 @@ func TestNewSchemaValidation(t *testing.T) { name: "Valid schema with comments", schemaName: "products", fields: []Field{ - {Name: "id", Type: FieldTypeInt64, Comment: "产品ID"}, - {Name: "name", Type: FieldTypeString, Comment: "产品名称"}, - {Name: "price", Type: FieldTypeFloat, Comment: "价格"}, + {Name: "id", Type: Int64, Comment: "产品ID"}, + {Name: "name", Type: String, Comment: "产品名称"}, + {Name: "price", Type: Float64, Comment: "价格"}, }, shouldError: false, }, @@ -260,10 +260,10 @@ func TestNewSchemaValidation(t *testing.T) { func TestNewSchemaFieldValidation(t *testing.T) { t.Run("Multiple duplicate field names", func(t *testing.T) { schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "name", Type: FieldTypeString}, - {Name: "id", Type: FieldTypeString}, // First duplicate - {Name: "name", Type: FieldTypeString}, // Second duplicate + {Name: "id", Type: Int64}, + {Name: "name", Type: String}, + {Name: "id", Type: String}, // First duplicate + {Name: "name", Type: String}, // Second duplicate }) if err == nil { @@ -284,9 +284,9 @@ func TestNewSchemaFieldValidation(t *testing.T) { t.Run("Case sensitive field names", func(t *testing.T) { // 大小写敏感,ID 和 id 应该是不同的字段 schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64}, - {Name: "ID", Type: FieldTypeInt64}, - {Name: "Id", Type: FieldTypeInt64}, + {Name: "id", Type: Int64}, + {Name: "ID", Type: Int64}, + {Name: "Id", Type: Int64}, }) if err != nil { @@ -301,10 +301,10 @@ func TestNewSchemaFieldValidation(t *testing.T) { t.Run("Fields with all types", func(t *testing.T) { schema, err := NewSchema("test", []Field{ - {Name: "int_field", Type: FieldTypeInt64}, - {Name: "string_field", Type: FieldTypeString}, - {Name: "float_field", Type: FieldTypeFloat}, - {Name: "bool_field", Type: FieldTypeBool}, + {Name: "int_field", Type: Int64}, + {Name: "string_field", Type: String}, + {Name: "float_field", Type: Float64}, + {Name: "bool_field", Type: Bool}, }) if err != nil { @@ -318,10 +318,10 @@ func TestNewSchemaFieldValidation(t *testing.T) { // 验证每个字段的类型 expectedTypes := map[string]FieldType{ - "int_field": FieldTypeInt64, - "string_field": FieldTypeString, - "float_field": FieldTypeFloat, - "bool_field": FieldTypeBool, + "int_field": Int64, + "string_field": String, + "float_field": Float64, + "bool_field": Bool, } for _, field := range schema.Fields { @@ -342,7 +342,7 @@ func TestNewSchemaEdgeCases(t *testing.T) { t.Run("Very long schema name", func(t *testing.T) { longName := strings.Repeat("a", 1000) schema, err := NewSchema(longName, []Field{ - {Name: "id", Type: FieldTypeInt64}, + {Name: "id", Type: Int64}, }) if err != nil { @@ -358,7 +358,7 @@ func TestNewSchemaEdgeCases(t *testing.T) { t.Run("Very long field name", func(t *testing.T) { longFieldName := strings.Repeat("b", 1000) schema, err := NewSchema("test", []Field{ - {Name: longFieldName, Type: FieldTypeInt64}, + {Name: longFieldName, Type: Int64}, }) if err != nil { @@ -376,7 +376,7 @@ func TestNewSchemaEdgeCases(t *testing.T) { for i := 0; i < 100; i++ { fields[i] = Field{ Name: strings.Repeat("field", 1) + string(rune('a'+i)), - Type: FieldTypeInt64, + Type: Int64, } } @@ -394,9 +394,9 @@ func TestNewSchemaEdgeCases(t *testing.T) { t.Run("Field with special characters", func(t *testing.T) { schema, err := NewSchema("test", []Field{ - {Name: "field_with_underscore", Type: FieldTypeInt64}, - {Name: "field123", Type: FieldTypeInt64}, - {Name: "字段名", Type: FieldTypeString}, // 中文字段名 + {Name: "field_with_underscore", Type: Int64}, + {Name: "field123", Type: Int64}, + {Name: "字段名", Type: String}, // 中文字段名 }) if err != nil { @@ -414,9 +414,9 @@ func TestNewSchemaEdgeCases(t *testing.T) { func TestNewSchemaConsistency(t *testing.T) { t.Run("Field order preserved", func(t *testing.T) { fields := []Field{ - {Name: "zebra", Type: FieldTypeString}, - {Name: "alpha", Type: FieldTypeInt64}, - {Name: "beta", Type: FieldTypeFloat}, + {Name: "zebra", Type: String}, + {Name: "alpha", Type: Int64}, + {Name: "beta", Type: Float64}, } schema, err := NewSchema("test", fields) @@ -437,8 +437,8 @@ func TestNewSchemaConsistency(t *testing.T) { t.Run("Field properties preserved", func(t *testing.T) { fields := []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "Primary key"}, - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "User name"}, + {Name: "id", Type: Int64, Indexed: true, Comment: "Primary key"}, + {Name: "name", Type: String, Indexed: false, Comment: "User name"}, } schema, err := NewSchema("users", fields) @@ -466,8 +466,8 @@ func TestNewSchemaConsistency(t *testing.T) { func TestExtractIndexValue(t *testing.T) { schema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "名称"}, - {Name: "age", Type: FieldTypeInt64, Indexed: true, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "名称"}, + {Name: "age", Type: Int64, Indexed: true, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -534,16 +534,16 @@ func TestChecksumDeterminism(t *testing.T) { // 创建相同的 Schema 多次 for i := range 10 { s1, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, }) if err != nil { t.Fatal(err) } s2, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -570,16 +570,16 @@ func TestChecksumDeterminism(t *testing.T) { // TestChecksumFieldOrderIndependent 测试字段顺序不影响 checksum func TestChecksumFieldOrderIndependent(t *testing.T) { s1, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, }) if err != nil { t.Fatal(err) } s2, err := NewSchema("users", []Field{ - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, }) if err != nil { t.Fatal(err) @@ -599,14 +599,14 @@ func TestChecksumFieldOrderIndependent(t *testing.T) { // TestChecksumDifferentData 测试不同 Schema 的 checksum 应该不同 func TestChecksumDifferentData(t *testing.T) { s1, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: true, Comment: "用户名"}, + {Name: "name", Type: String, Indexed: true, Comment: "用户名"}, }) if err != nil { t.Fatal(err) } s2, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, // Indexed 不同 + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, // Indexed 不同 }) if err != nil { t.Fatal(err) @@ -625,10 +625,10 @@ func TestChecksumDifferentData(t *testing.T) { // TestChecksumMultipleFieldOrders 测试多个字段的各种排列组合都产生相同 checksum func TestChecksumMultipleFieldOrders(t *testing.T) { // 定义 4 个字段 - fieldA := Field{Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "ID"} - fieldB := Field{Name: "name", Type: FieldTypeString, Indexed: false, Comment: "名称"} - fieldC := Field{Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"} - fieldD := Field{Name: "email", Type: FieldTypeString, Indexed: true, Comment: "邮箱"} + fieldA := Field{Name: "id", Type: Int64, Indexed: true, Comment: "ID"} + fieldB := Field{Name: "name", Type: String, Indexed: false, Comment: "名称"} + fieldC := Field{Name: "age", Type: Int64, Indexed: false, Comment: "年龄"} + fieldD := Field{Name: "email", Type: String, Indexed: true, Comment: "邮箱"} // 创建不同顺序的 Schema mustNewSchema := func(name string, fields []Field) *Schema { @@ -697,11 +697,11 @@ func TestStructToFields(t *testing.T) { Indexed bool Comment string }{ - "name": {FieldTypeString, true, "用户名"}, - "age": {FieldTypeInt64, false, "年龄"}, - "email": {FieldTypeString, true, "邮箱"}, - "score": {FieldTypeFloat, false, "分数"}, - "active": {FieldTypeBool, false, "是否激活"}, + "name": {String, true, "用户名"}, + "age": {Int64, false, "年龄"}, + "email": {String, true, "邮箱"}, + "score": {Float64, false, "分数"}, + "active": {Bool, false, "是否激活"}, } for _, field := range fields { @@ -834,30 +834,32 @@ func TestStructToFieldsAllTypes(t *testing.T) { t.Errorf("Expected 14 fields, got %d", len(fields)) } - // 验证所有整数类型都映射到 FieldTypeInt64 - intFields := []string{"int", "int64", "int32", "int16", "int8", "uint", "uint64", "uint32", "uint16", "uint8"} - for _, name := range intFields { - found := false - for _, field := range fields { - if field.Name == name { - found = true - if field.Type != FieldTypeInt64 { - t.Errorf("Field %s: expected FieldTypeInt64, got %v", name, field.Type) - } - break - } - } - if !found { - t.Errorf("Field %s not found", name) - } + // 验证所有类型都精确映射到对应的 FieldType + expectedTypes := map[string]FieldType{ + "int": Int, + "int64": Int64, + "int32": Int32, + "int16": Int16, + "int8": Int8, + "uint": Uint, + "uint64": Uint64, + "uint32": Uint32, + "uint16": Uint16, + "uint8": Uint8, + "string": String, + "float64": Float64, + "float32": Float32, + "bool": Bool, } - // 验证浮点类型 for _, field := range fields { - if field.Name == "float64" || field.Name == "float32" { - if field.Type != FieldTypeFloat { - t.Errorf("Field %s: expected FieldTypeFloat, got %v", field.Name, field.Type) - } + expectedType, exists := expectedTypes[field.Name] + if !exists { + t.Errorf("Unexpected field: %s", field.Name) + continue + } + if field.Type != expectedType { + t.Errorf("Field %s: expected %v, got %v", field.Name, expectedType, field.Type) } } diff --git a/sstable.go b/sstable.go index afe7231..ded07a2 100644 --- a/sstable.go +++ b/sstable.go @@ -9,8 +9,10 @@ import ( "sort" "strings" "sync" + "time" "github.com/edsrzf/mmap-go" + "github.com/shopspring/decimal" ) const ( @@ -230,47 +232,101 @@ func encodeSSTableRowBinary(row *SSTableRow, schema *Schema) ([]byte, error) { // writeFieldBinaryValue 写入字段值(二进制格式) func writeFieldBinaryValue(buf *bytes.Buffer, typ FieldType, value any) error { switch typ { - case FieldTypeInt64: - var v int64 - switch val := value.(type) { - case int: - v = int64(val) - case int64: - v = val - case int32: - v = int64(val) - case int16: - v = int64(val) - case int8: - v = int64(val) - case float64: - v = int64(val) - default: - return fmt.Errorf("cannot convert %T to int64", value) + // 有符号整数类型 + case Int: + v, ok := value.(int) + if !ok { + return fmt.Errorf("expected int, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, int64(v)) + + case Int8: + v, ok := value.(int8) + if !ok { + return fmt.Errorf("expected int8, got %T", value) } return binary.Write(buf, binary.LittleEndian, v) - case FieldTypeFloat: - var v float64 - switch val := value.(type) { - case float64: - v = val - case float32: - v = float64(val) - default: - return fmt.Errorf("cannot convert %T to float64", value) + case Int16: + v, ok := value.(int16) + if !ok { + return fmt.Errorf("expected int16, got %T", value) } return binary.Write(buf, binary.LittleEndian, v) - case FieldTypeBool: - var b byte - if value.(bool) { - b = 1 + case Int32, Rune: + // rune 和 int32 底层类型相同(都是 int32) + v, ok := value.(int32) + if !ok { + return fmt.Errorf("expected int32 (or rune), got %T", value) } - return buf.WriteByte(b) + return binary.Write(buf, binary.LittleEndian, v) - case FieldTypeString: - s := value.(string) + case Int64: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("expected int64, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, v) + + // 无符号整数类型 + case Uint: + v, ok := value.(uint) + if !ok { + return fmt.Errorf("expected uint, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, uint64(v)) + + case Uint8, Byte: + // byte 和 uint8 底层类型相同 + v, ok := value.(uint8) + if !ok { + return fmt.Errorf("expected uint8 or byte, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, v) + + case Uint16: + v, ok := value.(uint16) + if !ok { + return fmt.Errorf("expected uint16, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, v) + + case Uint32: + v, ok := value.(uint32) + if !ok { + return fmt.Errorf("expected uint32, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, v) + + case Uint64: + v, ok := value.(uint64) + if !ok { + return fmt.Errorf("expected uint64, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, v) + + // 浮点类型 + case Float32: + v, ok := value.(float32) + if !ok { + return fmt.Errorf("expected float32, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, v) + + case Float64: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("expected float64, got %T", value) + } + return binary.Write(buf, binary.LittleEndian, v) + + // 字符串类型 + case String: + s, ok := value.(string) + if !ok { + return fmt.Errorf("expected string, got %T", value) + } // 写入长度 if err := binary.Write(buf, binary.LittleEndian, uint32(len(s))); err != nil { return err @@ -279,6 +335,55 @@ func writeFieldBinaryValue(buf *bytes.Buffer, typ FieldType, value any) error { _, err := buf.WriteString(s) return err + // 布尔类型 + case Bool: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("expected bool, got %T", value) + } + var b byte + if v { + b = 1 + } + return buf.WriteByte(b) + + // Decimal 类型 + case Decimal: + v, ok := value.(decimal.Decimal) + if !ok { + return fmt.Errorf("expected decimal.Decimal, got %T", value) + } + // 使用 MarshalBinary 序列化 + data, err := v.MarshalBinary() + if err != nil { + return fmt.Errorf("marshal decimal: %w", err) + } + // 写入长度 + if err := binary.Write(buf, binary.LittleEndian, uint32(len(data))); err != nil { + return err + } + // 写入数据 + _, err = buf.Write(data) + return err + + // 时间类型 + case Time: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("expected time.Time, got %T", value) + } + // 存储为 Unix 时间戳(秒,int64) + return binary.Write(buf, binary.LittleEndian, v.Unix()) + + // 时间间隔类型 + case Duration: + v, ok := value.(time.Duration) + if !ok { + return fmt.Errorf("expected time.Duration, got %T", value) + } + // 存储为纳秒(int64) + return binary.Write(buf, binary.LittleEndian, int64(v)) + default: return fmt.Errorf("unsupported field type: %d", typ) } @@ -287,14 +392,65 @@ func writeFieldBinaryValue(buf *bytes.Buffer, typ FieldType, value any) error { // writeFieldZeroValue 写入字段零值 func writeFieldZeroValue(buf *bytes.Buffer, typ FieldType) error { switch typ { - case FieldTypeInt64: + // 有符号整数类型 + case Int: return binary.Write(buf, binary.LittleEndian, int64(0)) - case FieldTypeFloat: - return binary.Write(buf, binary.LittleEndian, float64(0)) - case FieldTypeBool: - return buf.WriteByte(0) - case FieldTypeString: + case Int8: + return binary.Write(buf, binary.LittleEndian, int8(0)) + case Int16: + return binary.Write(buf, binary.LittleEndian, int16(0)) + case Int32, Rune: + return binary.Write(buf, binary.LittleEndian, int32(0)) + case Int64: + return binary.Write(buf, binary.LittleEndian, int64(0)) + + // 无符号整数类型 + case Uint: + return binary.Write(buf, binary.LittleEndian, uint64(0)) + case Uint8, Byte: + return binary.Write(buf, binary.LittleEndian, uint8(0)) + case Uint16: + return binary.Write(buf, binary.LittleEndian, uint16(0)) + case Uint32: return binary.Write(buf, binary.LittleEndian, uint32(0)) + case Uint64: + return binary.Write(buf, binary.LittleEndian, uint64(0)) + + // 浮点类型 + case Float32: + return binary.Write(buf, binary.LittleEndian, float32(0)) + case Float64: + return binary.Write(buf, binary.LittleEndian, float64(0)) + + // 字符串类型 + case String: + return binary.Write(buf, binary.LittleEndian, uint32(0)) + + // 布尔类型 + case Bool: + return buf.WriteByte(0) + + // Decimal 类型(零值) + case Decimal: + zero := decimal.Zero + data, err := zero.MarshalBinary() + if err != nil { + return fmt.Errorf("marshal zero decimal: %w", err) + } + if err := binary.Write(buf, binary.LittleEndian, uint32(len(data))); err != nil { + return err + } + _, err = buf.Write(data) + return err + + // 时间类型(零值:Unix epoch) + case Time: + return binary.Write(buf, binary.LittleEndian, int64(0)) + + // 时间间隔类型(零值) + case Duration: + return binary.Write(buf, binary.LittleEndian, int64(0)) + default: return fmt.Errorf("unsupported field type: %d", typ) } @@ -416,7 +572,58 @@ func decodeSSTableRowBinaryPartial(data []byte, schema *Schema, fields []string) // readFieldBinaryValue 读取字段值(二进制格式) func readFieldBinaryValue(buf *bytes.Reader, typ FieldType, keep bool) (any, error) { switch typ { - case FieldTypeInt64: + // 有符号整数类型 + case Int: + var v int64 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return int(v), nil + } + return nil, nil + + case Int8: + var v int8 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + case Int16: + var v int16 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + case Int32: + var v int32 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + case Rune: + var v int32 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return rune(v), nil + } + return nil, nil + + case Int64: var v int64 if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { return nil, err @@ -426,7 +633,79 @@ func readFieldBinaryValue(buf *bytes.Reader, typ FieldType, keep bool) (any, err } return nil, nil - case FieldTypeFloat: + // 无符号整数类型 + case Uint: + var v uint64 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return uint(v), nil + } + return nil, nil + + case Uint8: + var v uint8 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + case Byte: + var v uint8 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return byte(v), nil + } + return nil, nil + + case Uint16: + var v uint16 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + case Uint32: + var v uint32 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + case Uint64: + var v uint64 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + // 浮点类型 + case Float32: + var v float32 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + return v, nil + } + return nil, nil + + case Float64: var v float64 if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { return nil, err @@ -436,17 +715,8 @@ func readFieldBinaryValue(buf *bytes.Reader, typ FieldType, keep bool) (any, err } return nil, nil - case FieldTypeBool: - b, err := buf.ReadByte() - if err != nil { - return nil, err - } - if keep { - return b == 1, nil - } - return nil, nil - - case FieldTypeString: + // 字符串类型 + case String: var length uint32 if err := binary.Read(buf, binary.LittleEndian, &length); err != nil { return nil, err @@ -460,6 +730,60 @@ func readFieldBinaryValue(buf *bytes.Reader, typ FieldType, keep bool) (any, err } return nil, nil + // 布尔类型 + case Bool: + b, err := buf.ReadByte() + if err != nil { + return nil, err + } + if keep { + return b == 1, nil + } + return nil, nil + + // Decimal 类型 + case Decimal: + var length uint32 + if err := binary.Read(buf, binary.LittleEndian, &length); err != nil { + return nil, err + } + data := make([]byte, length) + if _, err := buf.Read(data); err != nil { + return nil, err + } + if keep { + var d decimal.Decimal + if err := d.UnmarshalBinary(data); err != nil { + return nil, fmt.Errorf("unmarshal decimal: %w", err) + } + return d, nil + } + return nil, nil + + // 时间类型 + case Time: + var v int64 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + // 从 Unix 时间戳(秒)转换为 time.Time + return time.Unix(v, 0), nil + } + return nil, nil + + // 时间间隔类型 + case Duration: + var v int64 + if err := binary.Read(buf, binary.LittleEndian, &v); err != nil { + return nil, err + } + if keep { + // 从纳秒转换为 time.Duration + return time.Duration(v), nil + } + return nil, nil + default: return nil, fmt.Errorf("unsupported field type: %d", typ) } diff --git a/sstable_test.go b/sstable_test.go index bcf95d7..400b8f1 100644 --- a/sstable_test.go +++ b/sstable_test.go @@ -17,8 +17,8 @@ func TestSSTable(t *testing.T) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -168,7 +168,7 @@ func TestSSTableHeaderSerialization(t *testing.T) { func BenchmarkSSTableGet(b *testing.B) { // 创建 Schema schema, err := NewSchema("test", []Field{ - {Name: "value", Type: FieldTypeInt64}, + {Name: "value", Type: Int64}, }) if err != nil { b.Fatal(err) @@ -212,9 +212,9 @@ func TestSSTableBinaryEncoding(t *testing.T) { schema := &Schema{ Name: "users", Fields: []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, - {Name: "email", Type: FieldTypeString}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, + {Name: "email", Type: String}, }, } @@ -262,9 +262,9 @@ func TestSSTableEncodingComparison(t *testing.T) { schema := &Schema{ Name: "users", Fields: []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, - {Name: "email", Type: FieldTypeString}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, + {Name: "email", Type: String}, }, } @@ -303,9 +303,9 @@ func BenchmarkSSTableBinaryEncoding(b *testing.B) { schema := &Schema{ Name: "users", Fields: []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, - {Name: "email", Type: FieldTypeString}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, + {Name: "email", Type: String}, }, } @@ -351,10 +351,10 @@ func TestSSTablePerFieldCompression(t *testing.T) { schema := &Schema{ Name: "users", Fields: []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false}, - {Name: "age", Type: FieldTypeInt64, Indexed: false}, - {Name: "email", Type: FieldTypeString, Indexed: false}, - {Name: "score", Type: FieldTypeFloat, Indexed: false}, + {Name: "name", Type: String, Indexed: false}, + {Name: "age", Type: Int64, Indexed: false}, + {Name: "email", Type: String, Indexed: false}, + {Name: "score", Type: Float64, Indexed: false}, }, } @@ -437,16 +437,16 @@ func TestSSTablePartialReadingPerformance(t *testing.T) { schema := &Schema{ Name: "events", Fields: []Field{ - {Name: "field1", Type: FieldTypeString, Indexed: false}, - {Name: "field2", Type: FieldTypeString, Indexed: false}, - {Name: "field3", Type: FieldTypeString, Indexed: false}, - {Name: "field4", Type: FieldTypeString, Indexed: false}, - {Name: "field5", Type: FieldTypeString, Indexed: false}, - {Name: "field6", Type: FieldTypeString, Indexed: false}, - {Name: "field7", Type: FieldTypeString, Indexed: false}, - {Name: "field8", Type: FieldTypeString, Indexed: false}, - {Name: "field9", Type: FieldTypeString, Indexed: false}, - {Name: "field10", Type: FieldTypeString, Indexed: false}, + {Name: "field1", Type: String, Indexed: false}, + {Name: "field2", Type: String, Indexed: false}, + {Name: "field3", Type: String, Indexed: false}, + {Name: "field4", Type: String, Indexed: false}, + {Name: "field5", Type: String, Indexed: false}, + {Name: "field6", Type: String, Indexed: false}, + {Name: "field7", Type: String, Indexed: false}, + {Name: "field8", Type: String, Indexed: false}, + {Name: "field9", Type: String, Indexed: false}, + {Name: "field10", Type: String, Indexed: false}, }, } diff --git a/table.go b/table.go index ad68bff..76acbe8 100644 --- a/table.go +++ b/table.go @@ -398,8 +398,8 @@ func (t *Table) insertSingle(data map[string]any) error { Data: data, } - // 3. 序列化 - rowData, err := json.Marshal(row) + // 3. 序列化(使用二进制格式,保留类型信息) + rowData, err := encodeSSTableRowBinary(row, t.schema) if err != nil { return err } @@ -437,12 +437,12 @@ func (t *Table) Get(seq int64) (*SSTableRow, error) { // 1. 先查 MemTable Manager (Active + Immutables) data, found := t.memtableManager.Get(seq) if found { - var row SSTableRow - err := json.Unmarshal(data, &row) + // 使用二进制解码 + row, err := decodeSSTableRowBinary(data, t.schema) if err != nil { return nil, err } - return &row, nil + return row, nil } // 2. 查询 SST 文件 @@ -454,24 +454,12 @@ func (t *Table) GetPartial(seq int64, fields []string) (*SSTableRow, error) { // 1. 先查 MemTable Manager (Active + Immutables) data, found := t.memtableManager.Get(seq) if found { - var row SSTableRow - err := json.Unmarshal(data, &row) + // 使用二进制解码(支持部分解码) + row, err := decodeSSTableRowBinaryPartial(data, t.schema, fields) if err != nil { return nil, err } - - // MemTable 中的数据已经完全解析,需要手动过滤字段 - if len(fields) > 0 { - filteredData := make(map[string]any) - for _, field := range fields { - if val, ok := row.Data[field]; ok { - filteredData[field] = val - } - } - row.Data = filteredData - } - - return &row, nil + return row, nil } // 2. 查询 SST 文件(按需解码) @@ -505,10 +493,10 @@ func (t *Table) flushImmutable(imm *ImmutableMemTable, walNumber int64) error { var rows []*SSTableRow iter := imm.NewIterator() for iter.Next() { - var row SSTableRow - err := json.Unmarshal(iter.Value(), &row) + // 使用二进制解码 + row, err := decodeSSTableRowBinary(iter.Value(), t.schema) if err == nil { - rows = append(rows, &row) + rows = append(rows, row) } } @@ -609,10 +597,10 @@ func (t *Table) recover() error { // 重放 WAL 到 Active MemTable for _, entry := range entries { - // 验证 Schema - var row SSTableRow - if err := json.Unmarshal(entry.Data, &row); err != nil { - return fmt.Errorf("failed to unmarshal row during recovery (seq=%d): %w", entry.Seq, err) + // 使用二进制解码验证 Schema + row, err := decodeSSTableRowBinary(entry.Data, t.schema) + if err != nil { + return fmt.Errorf("failed to decode row during recovery (seq=%d): %w", entry.Seq, err) } // 验证 Schema diff --git a/table_test.go b/table_test.go index e662f0b..1dc7de0 100644 --- a/table_test.go +++ b/table_test.go @@ -19,8 +19,8 @@ func TestTable(t *testing.T) { defer os.RemoveAll(dir) schema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -84,7 +84,7 @@ func TestTableRecover(t *testing.T) { defer os.RemoveAll(dir) schema, err := NewSchema("test", []Field{ - {Name: "value", Type: FieldTypeInt64, Indexed: false, Comment: "值"}, + {Name: "value", Type: Int64, Indexed: false, Comment: "值"}, }) if err != nil { t.Fatal(err) @@ -150,7 +150,7 @@ func TestTableFlush(t *testing.T) { defer os.RemoveAll(dir) schema, err := NewSchema("test", []Field{ - {Name: "data", Type: FieldTypeString, Indexed: false, Comment: "数据"}, + {Name: "data", Type: String, Indexed: false, Comment: "数据"}, }) if err != nil { t.Fatal(err) @@ -203,7 +203,7 @@ func BenchmarkTableInsert(b *testing.B) { defer os.RemoveAll(dir) schema, err := NewSchema("test", []Field{ - {Name: "value", Type: FieldTypeInt64, Indexed: false, Comment: "值"}, + {Name: "value", Type: Int64, Indexed: false, Comment: "值"}, }) if err != nil { b.Fatal(err) @@ -232,7 +232,7 @@ func BenchmarkTableGet(b *testing.B) { defer os.RemoveAll(dir) schema, err := NewSchema("test", []Field{ - {Name: "value", Type: FieldTypeInt64, Indexed: false, Comment: "值"}, + {Name: "value", Type: Int64, Indexed: false, Comment: "值"}, }) if err != nil { b.Fatal(err) @@ -267,7 +267,7 @@ func TestHighConcurrencyWrite(t *testing.T) { // Note: This test uses []byte payload - we create a minimal schema // Schema validation accepts []byte as it gets JSON-marshaled schema, err := NewSchema("test", []Field{ - {Name: "worker_id", Type: FieldTypeInt64, Indexed: false, Comment: "Worker ID"}, + {Name: "worker_id", Type: Int64, Indexed: false, Comment: "Worker ID"}, }) if err != nil { t.Fatal(err) @@ -385,7 +385,7 @@ func TestConcurrentReadWrite(t *testing.T) { // Note: This test uses []byte data - we create a minimal schema schema, err := NewSchema("test", []Field{ - {Name: "writer_id", Type: FieldTypeInt64, Indexed: false, Comment: "Writer ID"}, + {Name: "writer_id", Type: Int64, Indexed: false, Comment: "Writer ID"}, }) if err != nil { t.Fatal(err) @@ -507,7 +507,7 @@ func TestPowerFailureRecovery(t *testing.T) { // Note: This test uses []byte data - we create a minimal schema schema, err := NewSchema("test", []Field{ - {Name: "batch", Type: FieldTypeInt64, Indexed: false, Comment: "Batch number"}, + {Name: "batch", Type: Int64, Indexed: false, Comment: "Batch number"}, }) if err != nil { t.Fatal(err) @@ -646,7 +646,7 @@ func TestCrashDuringCompaction(t *testing.T) { // Note: This test uses []byte data - we create a minimal schema schema, err := NewSchema("test", []Field{ - {Name: "index", Type: FieldTypeInt64, Indexed: false, Comment: "Index"}, + {Name: "index", Type: Int64, Indexed: false, Comment: "Index"}, }) if err != nil { t.Fatal(err) @@ -749,7 +749,7 @@ func TestLargeDataIntegrity(t *testing.T) { // Note: This test uses []byte data - we create a minimal schema schema, err := NewSchema("test", []Field{ - {Name: "size", Type: FieldTypeInt64, Indexed: false, Comment: "Size"}, + {Name: "size", Type: Int64, Indexed: false, Comment: "Size"}, }) if err != nil { t.Fatal(err) @@ -863,7 +863,7 @@ func BenchmarkConcurrentWrites(b *testing.B) { // Note: This benchmark uses []byte data - we create a minimal schema schema, err := NewSchema("test", []Field{ - {Name: "timestamp", Type: FieldTypeInt64, Indexed: false, Comment: "Timestamp"}, + {Name: "timestamp", Type: Int64, Indexed: false, Comment: "Timestamp"}, }) if err != nil { b.Fatal(err) @@ -917,9 +917,9 @@ func TestTableWithCompaction(t *testing.T) { tmpDir := t.TempDir() schema, err := NewSchema("test", []Field{ - {Name: "batch", Type: FieldTypeInt64, Indexed: false, Comment: "批次"}, - {Name: "index", Type: FieldTypeInt64, Indexed: false, Comment: "索引"}, - {Name: "value", Type: FieldTypeString, Indexed: false, Comment: "值"}, + {Name: "batch", Type: Int64, Indexed: false, Comment: "批次"}, + {Name: "index", Type: Int64, Indexed: false, Comment: "索引"}, + {Name: "value", Type: String, Indexed: false, Comment: "值"}, }) if err != nil { t.Fatal(err) @@ -1046,9 +1046,9 @@ func TestTableCompactionMerge(t *testing.T) { tmpDir := t.TempDir() schema, err := NewSchema("test", []Field{ - {Name: "batch", Type: FieldTypeInt64, Indexed: false, Comment: "批次"}, - {Name: "index", Type: FieldTypeInt64, Indexed: false, Comment: "索引"}, - {Name: "value", Type: FieldTypeString, Indexed: false, Comment: "值"}, + {Name: "batch", Type: Int64, Indexed: false, Comment: "批次"}, + {Name: "index", Type: Int64, Indexed: false, Comment: "索引"}, + {Name: "value", Type: String, Indexed: false, Comment: "值"}, }) if err != nil { t.Fatal(err) @@ -1154,8 +1154,8 @@ func TestTableBackgroundCompaction(t *testing.T) { tmpDir := t.TempDir() schema, err := NewSchema("test", []Field{ - {Name: "batch", Type: FieldTypeInt64, Indexed: false, Comment: "批次"}, - {Name: "index", Type: FieldTypeInt64, Indexed: false, Comment: "索引"}, + {Name: "batch", Type: Int64, Indexed: false, Comment: "批次"}, + {Name: "index", Type: Int64, Indexed: false, Comment: "索引"}, }) if err != nil { t.Fatal(err) @@ -1244,8 +1244,8 @@ func BenchmarkTableWithCompaction(b *testing.B) { tmpDir := b.TempDir() schema, err := NewSchema("test", []Field{ - {Name: "index", Type: FieldTypeInt64, Indexed: false, Comment: "索引"}, - {Name: "value", Type: FieldTypeString, Indexed: false, Comment: "值"}, + {Name: "index", Type: Int64, Indexed: false, Comment: "索引"}, + {Name: "value", Type: String, Indexed: false, Comment: "值"}, }) if err != nil { b.Fatal(err) @@ -1299,9 +1299,9 @@ func TestTableSchemaRecover(t *testing.T) { // 创建 Schema s, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, - {Name: "email", Type: FieldTypeString, Indexed: false, Comment: "邮箱"}, + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, + {Name: "email", Type: String, Indexed: false, Comment: "邮箱"}, }) if err != nil { t.Fatal(err) @@ -1374,8 +1374,8 @@ func TestTableSchemaRecoverInvalid(t *testing.T) { defer os.RemoveAll(dir) schema, err := NewSchema("test", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, - {Name: "age", Type: FieldTypeString, Indexed: false, Comment: "年龄字符串"}, + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, + {Name: "age", Type: String, Indexed: false, Comment: "年龄字符串"}, }) if err != nil { t.Fatal(err) @@ -1421,8 +1421,8 @@ func TestTableSchemaRecoverInvalid(t *testing.T) { // 3. 创建 Schema,age 字段要求 int64 s, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -1455,8 +1455,8 @@ func TestTableAutoRecoverSchema(t *testing.T) { // 创建 Schema s, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -1550,8 +1550,8 @@ func TestTableSchemaTamperDetection(t *testing.T) { // 创建 Schema s, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "用户名"}, - {Name: "age", Type: FieldTypeInt64, Indexed: false, Comment: "年龄"}, + {Name: "name", Type: String, Indexed: false, Comment: "用户名"}, + {Name: "age", Type: Int64, Indexed: false, Comment: "年龄"}, }) if err != nil { t.Fatal(err) @@ -1615,8 +1615,8 @@ func TestTableClean(t *testing.T) { defer db.Close() schema, err := NewSchema("users", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: true, Comment: "ID"}, - {Name: "name", Type: FieldTypeString, Indexed: false, Comment: "Name"}, + {Name: "id", Type: Int64, Indexed: true, Comment: "ID"}, + {Name: "name", Type: String, Indexed: false, Comment: "Name"}, }) if err != nil { t.Fatal(err) @@ -1687,7 +1687,7 @@ func TestTableDestroy(t *testing.T) { defer db.Close() schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, }) if err != nil { t.Fatal(err) @@ -1746,9 +1746,9 @@ func TestTableCleanWithIndex(t *testing.T) { defer db.Close() schema, err := 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"}, + {Name: "id", Type: Int64, Indexed: true, Comment: "ID"}, + {Name: "email", Type: String, Indexed: true, Comment: "Email"}, + {Name: "name", Type: String, Indexed: false, Comment: "Name"}, }) if err != nil { t.Fatal(err) @@ -1835,8 +1835,8 @@ func TestTableCleanAndQuery(t *testing.T) { defer db.Close() schema, err := NewSchema("test", []Field{ - {Name: "id", Type: FieldTypeInt64, Indexed: false, Comment: "ID"}, - {Name: "status", Type: FieldTypeString, Indexed: false, Comment: "Status"}, + {Name: "id", Type: Int64, Indexed: false, Comment: "ID"}, + {Name: "status", Type: String, Indexed: false, Comment: "Status"}, }) if err != nil { t.Fatal(err) @@ -1923,8 +1923,8 @@ func TestInsertMap(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -1968,8 +1968,8 @@ func TestInsertMapSlice(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -2019,9 +2019,9 @@ func TestInsertStruct(t *testing.T) { } schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, - {Name: "email", Type: FieldTypeString}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, + {Name: "email", Type: String}, }) if err != nil { t.Fatal(err) @@ -2077,9 +2077,9 @@ func TestInsertStructPointer(t *testing.T) { } schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, - {Name: "email", Type: FieldTypeString}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, + {Name: "email", Type: String}, }) if err != nil { t.Fatal(err) @@ -2131,8 +2131,8 @@ func TestInsertStructSlice(t *testing.T) { } schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -2183,8 +2183,8 @@ func TestInsertStructPointerSlice(t *testing.T) { } schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) @@ -2237,9 +2237,9 @@ func TestInsertWithSnakeCase(t *testing.T) { } schema, err := NewSchema("users", []Field{ - {Name: "user_name", Type: FieldTypeString, Comment: "用户名"}, - {Name: "email_address", Type: FieldTypeString}, - {Name: "is_active", Type: FieldTypeBool}, + {Name: "user_name", Type: String, Comment: "用户名"}, + {Name: "email_address", Type: String}, + {Name: "is_active", Type: Bool}, }) if err != nil { t.Fatal(err) @@ -2292,7 +2292,7 @@ func TestInsertInvalidType(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, + {Name: "name", Type: String}, }) if err != nil { t.Fatal(err) @@ -2333,7 +2333,7 @@ func TestInsertEmptySlice(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, + {Name: "name", Type: String}, }) if err != nil { t.Fatal(err) @@ -2370,8 +2370,8 @@ func TestBatchInsertPerformance(t *testing.T) { defer os.RemoveAll(tmpDir) schema, err := NewSchema("users", []Field{ - {Name: "name", Type: FieldTypeString}, - {Name: "age", Type: FieldTypeInt64}, + {Name: "name", Type: String}, + {Name: "age", Type: Int64}, }) if err != nil { t.Fatal(err) diff --git a/webui/webui.go b/webui/webui.go index 0bf1dbe..1a70c5f 100644 --- a/webui/webui.go +++ b/webui/webui.go @@ -415,7 +415,7 @@ func (ui *WebUI) handleTableData(w http.ResponseWriter, r *http.Request, tableNa // 检查字段类型 field, err := tableSchema.GetField(k) - if err == nil && field.Type == srdb.FieldTypeString { + if err == nil && field.Type == srdb.String { // 对字符串字段进行剪裁 if str, ok := v.(string); ok { runes := []rune(str)