功能:添加可空字段和标签格式支持
- 实现可空字段(nullable)功能 - 支持 *int, *string 等指针类型 - 添加 nullable 示例程序 - 完善可空字段验证测试 - 添加标签格式(tag format)支持 - 支持自定义字段标签 - 添加 tag_format 示例程序 - 增强 Schema 标签解析能力 - 优化 Schema 和 SSTable 处理逻辑 - 添加诊断工具测试用例
This commit is contained in:
353
examples/nullable/README.md
Normal file
353
examples/nullable/README.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# Nullable 字段支持
|
||||
|
||||
## 概述
|
||||
|
||||
SRDB 通过**指针类型**来声明 nullable 字段:
|
||||
|
||||
- **指针类型** (`*string`, `*int32`, ...) - 自动推断为 nullable
|
||||
- **Tag 标记** (`nullable`) - 可选,仅用于指针类型(非指针类型会报错)
|
||||
|
||||
## 方式 1: 指针类型(推荐)
|
||||
|
||||
### 定义
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID uint32 `srdb:"field:id"`
|
||||
Name string `srdb:"field:name"`
|
||||
Email *string `srdb:"field:email"` // 自动推断为 nullable
|
||||
Age *int32 `srdb:"field:age"` // 自动推断为 nullable
|
||||
}
|
||||
```
|
||||
|
||||
### 使用
|
||||
|
||||
```go
|
||||
// 插入数据
|
||||
table.Insert(map[string]any{
|
||||
"id": uint32(1),
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com", // 有值
|
||||
"age": int32(25),
|
||||
})
|
||||
|
||||
table.Insert(map[string]any{
|
||||
"id": uint32(2),
|
||||
"name": "Bob",
|
||||
"email": nil, // NULL
|
||||
"age": nil,
|
||||
})
|
||||
|
||||
// 查询数据
|
||||
rows, _ := table.Query().Rows()
|
||||
for rows.Next() {
|
||||
data := rows.Row().Data()
|
||||
|
||||
if data["email"] != nil {
|
||||
fmt.Println("Email:", data["email"])
|
||||
} else {
|
||||
fmt.Println("Email: <NULL>")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✓ Go 原生支持
|
||||
- ✓ nil 天然表示 NULL
|
||||
- ✓ 最符合 Go 习惯
|
||||
- ✓ 无需额外依赖
|
||||
- ✓ StructToFields 自动识别
|
||||
|
||||
**使用场景**:
|
||||
- 大部分 nullable 字段场景
|
||||
- 新项目
|
||||
|
||||
---
|
||||
|
||||
## Tag 显式标记(可选)
|
||||
|
||||
### 定义
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID uint32 `srdb:"field:id"`
|
||||
Name string `srdb:"field:name"`
|
||||
Email *string `srdb:"field:email"` // 指针类型,自动 nullable
|
||||
Phone *string `srdb:"field:phone;nullable"` // 显式标记(冗余但允许)
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **重要**:`nullable` 标记**只能用于指针类型**。非指针类型标记 `nullable` 会报错:
|
||||
|
||||
```go
|
||||
// ✗ 错误:非指针类型不能标记 nullable
|
||||
type Wrong struct {
|
||||
Email string `srdb:"field:email;nullable"` // 报错!
|
||||
}
|
||||
|
||||
// ✓ 正确:必须是指针类型
|
||||
type Correct struct {
|
||||
Email *string `srdb:"field:email;nullable"` // ✓ 或省略 nullable
|
||||
}
|
||||
```
|
||||
|
||||
**为什么要这样设计?**
|
||||
- 保持类型系统的一致性
|
||||
- 避免 "string 类型但允许 NULL" 这种混乱的语义
|
||||
- 强制使用指针类型来表示 nullable,语义更清晰
|
||||
|
||||
---
|
||||
|
||||
## 完整示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"code.tczkiot.com/wlw/srdb"
|
||||
)
|
||||
|
||||
// 用户表(使用指针)
|
||||
type User struct {
|
||||
ID uint32 `srdb:"field:id"`
|
||||
Name string `srdb:"field:name"`
|
||||
Email *string `srdb:"field:email;comment:邮箱(可选)"`
|
||||
Phone *string `srdb:"field:phone;comment:手机号(可选)"`
|
||||
Age *int32 `srdb:"field:age;comment:年龄(可选)"`
|
||||
CreatedAt time.Time `srdb:"field:created_at"`
|
||||
}
|
||||
|
||||
// 商品表(使用指针)
|
||||
type Product struct {
|
||||
ID uint32 `srdb:"field:id"`
|
||||
Name string `srdb:"field:name;indexed"`
|
||||
Price *float64 `srdb:"field:price;comment:价格(可选)"`
|
||||
Stock *int32 `srdb:"field:stock;comment:库存(可选)"`
|
||||
Description *string `srdb:"field:description"`
|
||||
CreatedAt time.Time `srdb:"field:created_at"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := srdb.Open("./data")
|
||||
defer db.Close()
|
||||
|
||||
// 创建用户表
|
||||
userFields, _ := srdb.StructToFields(User{})
|
||||
userSchema, _ := srdb.NewSchema("users", userFields)
|
||||
userTable, _ := db.CreateTable("users", userSchema)
|
||||
|
||||
// 插入用户
|
||||
userTable.Insert(map[string]any{
|
||||
"id": uint32(1),
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"phone": "13800138000",
|
||||
"age": int32(25),
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
|
||||
userTable.Insert(map[string]any{
|
||||
"id": uint32(2),
|
||||
"name": "Bob",
|
||||
"email": nil, // NULL
|
||||
"phone": nil,
|
||||
"age": nil,
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
|
||||
// 查询用户
|
||||
rows, _ := userTable.Query().Rows()
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
data := rows.Row().Data()
|
||||
|
||||
fmt.Printf("%s:", data["name"])
|
||||
|
||||
if data["email"] != nil {
|
||||
fmt.Printf(" email=%s", data["email"])
|
||||
} else {
|
||||
fmt.Print(" email=<NULL>")
|
||||
}
|
||||
|
||||
if data["age"] != nil {
|
||||
fmt.Printf(", age=%d", data["age"])
|
||||
} else {
|
||||
fmt.Print(", age=<NULL>")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用指针类型
|
||||
|
||||
```go
|
||||
// ✓ 推荐:指针类型,无需 tag
|
||||
type User struct {
|
||||
Email *string
|
||||
Phone *string
|
||||
}
|
||||
|
||||
// ✓ 可以:显式标记(冗余但允许)
|
||||
type User struct {
|
||||
Email *string `srdb:"nullable"`
|
||||
Phone *string `srdb:"nullable"`
|
||||
}
|
||||
|
||||
// ✗ 错误:非指针类型不能标记 nullable
|
||||
type User struct {
|
||||
Email string `srdb:"nullable"` // 报错!
|
||||
Phone string `srdb:"nullable"` // 报错!
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 添加注释说明
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Email *string `srdb:"field:email;comment:邮箱(可选)"`
|
||||
Phone *string `srdb:"field:phone;comment:手机号(可选)"`
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 一致性
|
||||
|
||||
在同一个结构体中,尽量使用统一的方式:
|
||||
|
||||
```go
|
||||
// ✓ 好:统一使用指针
|
||||
type User struct {
|
||||
Email *string
|
||||
Phone *string
|
||||
Age *int32
|
||||
}
|
||||
|
||||
// ✗ 避免:混用
|
||||
type User struct {
|
||||
Email *string
|
||||
Phone string `srdb:"nullable"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前限制
|
||||
|
||||
⚠️ **注意**:当前版本在二进制编码中,NULL 值会被存储为零值。这意味着:
|
||||
|
||||
- `0` 和 `NULL` 在 int 类型中无法区分
|
||||
- `""` 和 `NULL` 在 string 类型中无法区分
|
||||
- `false` 和 `NULL` 在 bool 类型中无法区分
|
||||
|
||||
**未来改进** (v2.1):
|
||||
我们计划在二进制编码格式中添加 NULL 标志位,完全区分零值和 NULL。
|
||||
|
||||
**当前解决方案**:
|
||||
- 对于整数类型,考虑使用特殊值(如 -1)表示未设置
|
||||
- 对于字符串,考虑使用非空默认值
|
||||
- 或等待 v2.1 版本的完整 NULL 支持
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: 为什么推荐指针类型?
|
||||
|
||||
**A**: 指针类型是 Go 语言表示 nullable 的标准方式:
|
||||
- nil 天然表示 NULL
|
||||
- 类型系统原生支持
|
||||
- 无需额外学习成本
|
||||
- StructToFields 自动识别
|
||||
|
||||
### Q: nullable tag 是必需的吗?
|
||||
|
||||
**A**: 不是。指针类型会自动推断为 nullable,无需显式标记。
|
||||
|
||||
```go
|
||||
// 这两种写法等价
|
||||
type User struct {
|
||||
Email *string // 自动 nullable
|
||||
Phone *string `srdb:"nullable"` // 显式标记(冗余)
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 非指针类型可以标记 nullable 吗?
|
||||
|
||||
**A**: 不可以!非指针类型标记 `nullable` 会报错:
|
||||
|
||||
```go
|
||||
// ✗ 错误
|
||||
type User struct {
|
||||
Email string `srdb:"nullable"` // 报错!
|
||||
}
|
||||
|
||||
// ✓ 正确
|
||||
type User struct {
|
||||
Email *string // 或 *string `srdb:"nullable"`
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:保持类型系统的一致性,避免混乱的语义。
|
||||
|
||||
### Q: 插入时可以省略 nullable 字段吗?
|
||||
|
||||
**A**: 可以。如果 map 中不包含某个 nullable 字段,会被视为 NULL。
|
||||
|
||||
```go
|
||||
// 这两种写法等价
|
||||
table.Insert(map[string]any{
|
||||
"name": "Alice",
|
||||
"email": nil,
|
||||
})
|
||||
|
||||
table.Insert(map[string]any{
|
||||
"name": "Alice",
|
||||
// email 字段省略,自动为 NULL
|
||||
})
|
||||
```
|
||||
|
||||
### Q: NULL 和零值的问题何时解决?
|
||||
|
||||
**A**: 计划在 v2.1 版本中添加 NULL 标志位,完全区分 NULL 和零值。
|
||||
|
||||
---
|
||||
|
||||
## 运行示例
|
||||
|
||||
```bash
|
||||
cd examples/nullable
|
||||
go run main.go
|
||||
```
|
||||
|
||||
输出:
|
||||
```
|
||||
=== Nullable 字段测试(指针类型) ===
|
||||
|
||||
【测试 1】用户表(指针类型)
|
||||
─────────────────────────────
|
||||
User Schema 字段:
|
||||
- id (uint32)
|
||||
- name (string)
|
||||
- email (string) [nullable] // 邮箱(可选)
|
||||
- phone (string) [nullable] // 手机号(可选)
|
||||
- age (int32) [nullable] // 年龄(可选)
|
||||
- created_at (time)
|
||||
|
||||
插入用户数据:
|
||||
✓ Alice (所有字段都有值)
|
||||
✓ Bob (email 和 age 为 NULL)
|
||||
✓ Charlie (所有可选字段都为 NULL)
|
||||
|
||||
查询结果:
|
||||
- Alice: email=alice@example.com, phone=13800138000, age=25
|
||||
- Bob: email=<NULL>, phone=13900139000, age=<NULL>
|
||||
- Charlie: email=<NULL>, phone=<NULL>, age=<NULL>
|
||||
```
|
||||
289
examples/nullable/main.go
Normal file
289
examples/nullable/main.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/srdb"
|
||||
)
|
||||
|
||||
// User 使用指针类型表示 nullable 字段
|
||||
type User struct {
|
||||
ID uint32 `srdb:"field:id"`
|
||||
Name string `srdb:"field:name"`
|
||||
Email *string `srdb:"field:email;comment:邮箱(可选)"`
|
||||
Phone *string `srdb:"field:phone;comment:手机号(可选)"`
|
||||
Age *int32 `srdb:"field:age;comment:年龄(可选)"`
|
||||
CreatedAt time.Time `srdb:"field:created_at"`
|
||||
}
|
||||
|
||||
// Product 商品表
|
||||
type Product struct {
|
||||
ID uint32 `srdb:"field:id"`
|
||||
Name string `srdb:"field:name;indexed"`
|
||||
Price *float64 `srdb:"field:price;comment:价格(可选)"`
|
||||
Stock *int32 `srdb:"field:stock;comment:库存(可选)"`
|
||||
Description *string `srdb:"field:description;comment:描述(可选)"`
|
||||
CreatedAt time.Time `srdb:"field:created_at"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== Nullable 字段测试(指针类型) ===\n")
|
||||
|
||||
dataPath := "./nullable_data"
|
||||
os.RemoveAll(dataPath)
|
||||
defer os.RemoveAll(dataPath)
|
||||
|
||||
db, err := srdb.Open(dataPath)
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// ==================== 测试 1: 用户表 ====================
|
||||
fmt.Println("【测试 1】用户表(指针类型)")
|
||||
fmt.Println("─────────────────────────────")
|
||||
|
||||
userFields, err := srdb.StructToFields(User{})
|
||||
if err != nil {
|
||||
log.Fatalf("生成 User 字段失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("User Schema 字段:")
|
||||
for _, f := range userFields {
|
||||
fmt.Printf(" - %s (%s)", f.Name, f.Type)
|
||||
if f.Nullable {
|
||||
fmt.Print(" [nullable]")
|
||||
}
|
||||
if f.Comment != "" {
|
||||
fmt.Printf(" // %s", f.Comment)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
userSchema, err := srdb.NewSchema("users", userFields)
|
||||
if err != nil {
|
||||
log.Fatalf("创建 User Schema 失败: %v", err)
|
||||
}
|
||||
|
||||
userTable, err := db.CreateTable("users", userSchema)
|
||||
if err != nil {
|
||||
log.Fatalf("创建 User 表失败: %v", err)
|
||||
}
|
||||
|
||||
// 插入数据(所有字段都有值)
|
||||
fmt.Println("\n插入用户数据:")
|
||||
err = userTable.Insert(map[string]any{
|
||||
"id": uint32(1),
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"phone": "13800138000",
|
||||
"age": int32(25),
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("插入用户失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ Alice (所有字段都有值)")
|
||||
|
||||
// 插入数据(部分字段为 NULL)
|
||||
err = userTable.Insert(map[string]any{
|
||||
"id": uint32(2),
|
||||
"name": "Bob",
|
||||
"email": nil, // NULL
|
||||
"phone": "13900139000",
|
||||
"age": nil, // NULL
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("插入用户失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ Bob (email 和 age 为 NULL)")
|
||||
|
||||
// 插入数据(全部可选字段为 NULL)
|
||||
err = userTable.Insert(map[string]any{
|
||||
"id": uint32(3),
|
||||
"name": "Charlie",
|
||||
"email": nil,
|
||||
"phone": nil,
|
||||
"age": nil,
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("插入用户失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ Charlie (所有可选字段都为 NULL)")
|
||||
|
||||
// 查询
|
||||
fmt.Println("\n查询结果:")
|
||||
rows, err := userTable.Query().Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
data := rows.Row().Data()
|
||||
|
||||
fmt.Printf(" - %s:", data["name"])
|
||||
|
||||
if email := data["email"]; email != nil {
|
||||
fmt.Printf(" email=%s", email)
|
||||
} else {
|
||||
fmt.Print(" email=<NULL>")
|
||||
}
|
||||
|
||||
if phone := data["phone"]; phone != nil {
|
||||
fmt.Printf(", phone=%s", phone)
|
||||
} else {
|
||||
fmt.Print(", phone=<NULL>")
|
||||
}
|
||||
|
||||
if age := data["age"]; age != nil {
|
||||
fmt.Printf(", age=%d", age)
|
||||
} else {
|
||||
fmt.Print(", age=<NULL>")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// ==================== 测试 2: 商品表 ====================
|
||||
fmt.Println("\n【测试 2】商品表(指针类型)")
|
||||
fmt.Println("─────────────────────────────")
|
||||
|
||||
productFields, err := srdb.StructToFields(Product{})
|
||||
if err != nil {
|
||||
log.Fatalf("生成 Product 字段失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Product Schema 字段:")
|
||||
for _, f := range productFields {
|
||||
fmt.Printf(" - %s (%s)", f.Name, f.Type)
|
||||
if f.Nullable {
|
||||
fmt.Print(" [nullable]")
|
||||
}
|
||||
if f.Indexed {
|
||||
fmt.Print(" [indexed]")
|
||||
}
|
||||
if f.Comment != "" {
|
||||
fmt.Printf(" // %s", f.Comment)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
productSchema, err := srdb.NewSchema("products", productFields)
|
||||
if err != nil {
|
||||
log.Fatalf("创建 Product Schema 失败: %v", err)
|
||||
}
|
||||
|
||||
productTable, err := db.CreateTable("products", productSchema)
|
||||
if err != nil {
|
||||
log.Fatalf("创建 Product 表失败: %v", err)
|
||||
}
|
||||
|
||||
// 插入商品
|
||||
fmt.Println("\n插入商品数据:")
|
||||
err = productTable.Insert(map[string]any{
|
||||
"id": uint32(101),
|
||||
"name": "iPhone 15",
|
||||
"price": 6999.0,
|
||||
"stock": int32(50),
|
||||
"description": "最新款智能手机",
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("插入商品失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ iPhone 15 (所有字段都有值)")
|
||||
|
||||
// 待定商品(价格和库存未定)
|
||||
err = productTable.Insert(map[string]any{
|
||||
"id": uint32(102),
|
||||
"name": "新品预告",
|
||||
"price": nil, // 价格未定
|
||||
"stock": nil, // 库存未定
|
||||
"description": "即将发布",
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("插入商品失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ 新品预告 (price 和 stock 为 NULL)")
|
||||
|
||||
// 查询商品
|
||||
fmt.Println("\n查询结果:")
|
||||
rows2, err := productTable.Query().Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows2.Close()
|
||||
|
||||
for rows2.Next() {
|
||||
data := rows2.Row().Data()
|
||||
|
||||
fmt.Printf(" - %s:", data["name"])
|
||||
|
||||
if price := data["price"]; price != nil {
|
||||
fmt.Printf(" price=%.2f", price)
|
||||
} else {
|
||||
fmt.Print(" price=<未定价>")
|
||||
}
|
||||
|
||||
if stock := data["stock"]; stock != nil {
|
||||
fmt.Printf(", stock=%d", stock)
|
||||
} else {
|
||||
fmt.Print(", stock=<未定>")
|
||||
}
|
||||
|
||||
if desc := data["description"]; desc != nil {
|
||||
fmt.Printf(", desc=%s", desc)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// ==================== 测试 3: 使用索引查询 ====================
|
||||
fmt.Println("\n【测试 3】使用索引查询")
|
||||
fmt.Println("─────────────────────────────")
|
||||
|
||||
// 再插入几个商品
|
||||
productTable.Insert(map[string]any{
|
||||
"id": uint32(103),
|
||||
"name": "MacBook Pro",
|
||||
"price": 12999.0,
|
||||
"stock": int32(20),
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
|
||||
productTable.Insert(map[string]any{
|
||||
"id": uint32(104),
|
||||
"name": "iPad Air",
|
||||
"price": 4999.0,
|
||||
"stock": nil, // 缺货
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
|
||||
// 按名称查询(使用索引)
|
||||
fmt.Println("\n按名称查询 'iPhone 15':")
|
||||
rows3, err := productTable.Query().Eq("name", "iPhone 15").Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows3.Close()
|
||||
|
||||
for rows3.Next() {
|
||||
data := rows3.Row().Data()
|
||||
fmt.Printf(" 找到: %s, price=%.2f\n", data["name"], data["price"])
|
||||
}
|
||||
|
||||
fmt.Println("\n=== 测试完成 ===")
|
||||
fmt.Println("\n✨ Nullable 支持总结:")
|
||||
fmt.Println(" • 使用指针类型 (*string, *int32, ...) 表示 nullable 字段")
|
||||
fmt.Println(" • StructToFields 自动识别指针类型并设置 nullable=true")
|
||||
fmt.Println(" • 插入时直接传值或 nil")
|
||||
fmt.Println(" • 查询时检查字段是否为 nil")
|
||||
fmt.Println(" • 简单、直观、符合 Go 习惯")
|
||||
}
|
||||
255
examples/tag_format/README.md
Normal file
255
examples/tag_format/README.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# SRDB 新 Tag 格式说明
|
||||
|
||||
## 概述
|
||||
|
||||
从 v2.0 开始,SRDB 支持新的结构体标签格式,采用 `key:value` 形式,使标签解析与顺序无关。
|
||||
|
||||
## 新格式特点
|
||||
|
||||
### 1. 顺序无关
|
||||
|
||||
旧格式(位置相关):
|
||||
```go
|
||||
type User struct {
|
||||
Name string `srdb:"name;comment:用户名;nullable"`
|
||||
Email string `srdb:"email;indexed;comment:邮箱;nullable"`
|
||||
}
|
||||
```
|
||||
|
||||
新格式(顺序无关):
|
||||
```go
|
||||
type User struct {
|
||||
// 以下三种写法完全等价
|
||||
Name string `srdb:"field:name;comment:用户名;nullable"`
|
||||
Email string `srdb:"comment:邮箱;field:email;indexed;nullable"`
|
||||
Age int64 `srdb:"nullable;field:age;comment:年龄;indexed"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 支持的标签
|
||||
|
||||
| 标签格式 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| `field:xxx` | 字段名(如不指定则使用结构体字段名) | `field:user_name` |
|
||||
| `comment:xxx` | 字段注释 | `comment:用户邮箱` |
|
||||
| `indexed` | 创建二级索引 | `indexed` |
|
||||
| `nullable` | 允许 NULL 值 | `nullable` |
|
||||
|
||||
### 3. 向后兼容
|
||||
|
||||
新格式完全兼容旧的位置相关格式:
|
||||
|
||||
```go
|
||||
// 旧格式(仍然有效)
|
||||
type Product struct {
|
||||
ID uint32 `srdb:"id;comment:商品ID"`
|
||||
Name string `srdb:"name;indexed;comment:商品名称"`
|
||||
}
|
||||
|
||||
// 新格式(推荐)
|
||||
type Product struct {
|
||||
ID uint32 `srdb:"field:id;comment:商品ID"`
|
||||
Name string `srdb:"field:name;indexed;comment:商品名称"`
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"code.tczkiot.com/wlw/srdb"
|
||||
)
|
||||
|
||||
// 使用新 tag 格式定义结构体
|
||||
type Product struct {
|
||||
ID uint32 `srdb:"field:id;comment:商品ID"`
|
||||
Name string `srdb:"comment:商品名称;field:name;indexed"`
|
||||
Price float64 `srdb:"field:price;nullable;comment:价格"`
|
||||
Stock int32 `srdb:"indexed;field:stock;comment:库存数量"`
|
||||
Category string `srdb:"field:category;indexed;nullable;comment:分类"`
|
||||
Description string `srdb:"nullable;field:description;comment:商品描述"`
|
||||
CreatedAt time.Time `srdb:"field:created_at;comment:创建时间"`
|
||||
UpdatedAt time.Time `srdb:"comment:更新时间;field:updated_at;nullable"`
|
||||
ExpireIn time.Duration `srdb:"field:expire_in;comment:过期时间;nullable"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 从结构体自动生成 Schema
|
||||
fields, err := srdb.StructToFields(Product{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
schema, err := srdb.NewSchema("products", fields)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 创建数据库和表
|
||||
db, err := srdb.Open("./data")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
table, err := db.CreateTable("products", schema)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 插入数据(nullable 字段可以使用 nil)
|
||||
err = table.Insert(map[string]any{
|
||||
"id": uint32(1001),
|
||||
"name": "iPhone 15",
|
||||
"price": 6999.0,
|
||||
"stock": int32(50),
|
||||
"category": "电子产品",
|
||||
"created_at": time.Now(),
|
||||
"expire_in": 365 * 24 * time.Hour,
|
||||
})
|
||||
|
||||
// nullable 字段设为 nil
|
||||
err = table.Insert(map[string]any{
|
||||
"id": uint32(1002),
|
||||
"name": "待定商品",
|
||||
"price": nil, // ✓ 允许 NULL(字段标记为 nullable)
|
||||
"stock": int32(0),
|
||||
"created_at": time.Now(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 类型映射
|
||||
|
||||
新 tag 格式支持 SRDB 的所有 19 种类型:
|
||||
|
||||
| Go 类型 | FieldType | 说明 |
|
||||
|---------|-----------|------|
|
||||
| `int`, `int8`, `int16`, `int32`, `int64` | `Int`, `Int8`, ... | 有符号整数 |
|
||||
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | `Uint`, `Uint8`, ... | 无符号整数 |
|
||||
| `byte` | `Byte` | 字节类型(底层 uint8) |
|
||||
| `rune` | `Rune` | 字符类型(底层 int32) |
|
||||
| `float32`, `float64` | `Float32`, `Float64` | 浮点数 |
|
||||
| `string` | `String` | 字符串 |
|
||||
| `bool` | `Bool` | 布尔值 |
|
||||
| `time.Time` | `Time` | 时间戳 |
|
||||
| `time.Duration` | `Duration` | 时间间隔 |
|
||||
| `decimal.Decimal` | `Decimal` | 高精度十进制 |
|
||||
|
||||
## Nullable 支持
|
||||
|
||||
标记为 `nullable` 的字段:
|
||||
- 可以接受 `nil` 值
|
||||
- 插入时可以省略该字段
|
||||
- 读取时需要检查是否为 `nil`
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Name string `srdb:"field:name;comment:必填字段"`
|
||||
Email string `srdb:"field:email;nullable;comment:可选字段"`
|
||||
}
|
||||
|
||||
// 插入数据
|
||||
table.Insert(map[string]any{
|
||||
"name": "Alice",
|
||||
"email": nil, // ✓ nullable 字段可以为 nil
|
||||
})
|
||||
|
||||
table.Insert(map[string]any{
|
||||
"name": "Bob",
|
||||
// email 可以省略
|
||||
})
|
||||
|
||||
// 查询数据
|
||||
rows, _ := table.Query().Rows()
|
||||
for rows.Next() {
|
||||
data := rows.Row().Data()
|
||||
if data["email"] != nil {
|
||||
fmt.Println("Email:", data["email"])
|
||||
} else {
|
||||
fmt.Println("Email: <未设置>")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 索引支持
|
||||
|
||||
标记为 `indexed` 的字段会自动创建二级索引:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID uint32 `srdb:"field:id"`
|
||||
Email string `srdb:"field:email;indexed;comment:邮箱(自动创建索引)"`
|
||||
}
|
||||
|
||||
// 查询时自动使用索引
|
||||
rows, _ := table.Query().Eq("email", "user@example.com").Rows()
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **优先使用新格式**:虽然兼容旧格式,但推荐使用 `field:xxx` 明确指定字段名
|
||||
2. **合理使用 nullable**:只对真正可选的字段标记 nullable,避免滥用
|
||||
3. **为索引字段添加注释**:标记 `indexed` 时说明索引用途
|
||||
4. **按语义排序标签**:建议按 `field → indexed → nullable → comment` 顺序编写,便于阅读
|
||||
|
||||
示例:
|
||||
```go
|
||||
type Product struct {
|
||||
ID uint32 `srdb:"field:id;comment:商品ID"`
|
||||
Name string `srdb:"field:name;indexed;comment:商品名称(索引)"`
|
||||
Price float64 `srdb:"field:price;nullable;comment:价格(可选)"`
|
||||
Category string `srdb:"field:category;indexed;nullable;comment:分类(索引+可选)"`
|
||||
}
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
从旧格式迁移到新格式:
|
||||
|
||||
```bash
|
||||
# 使用 sed 批量转换(示例)
|
||||
sed -i 's/`srdb:"\([^;]*\);/`srdb:"field:\1;/g' *.go
|
||||
```
|
||||
|
||||
或手动修改:
|
||||
|
||||
```go
|
||||
// 旧格式
|
||||
type User struct {
|
||||
Name string `srdb:"name;comment:用户名"`
|
||||
}
|
||||
|
||||
// 新格式
|
||||
type User struct {
|
||||
Name string `srdb:"field:name;comment:用户名"`
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
常见错误:
|
||||
|
||||
```go
|
||||
// ❌ 错误:field 值为空
|
||||
`srdb:"field:;comment:xxx"`
|
||||
|
||||
// ✓ 正确:省略 field 前缀(使用结构体字段名)
|
||||
`srdb:"comment:xxx"`
|
||||
|
||||
// ❌ 错误:重复的 key
|
||||
`srdb:"field:name;field:user_name"`
|
||||
|
||||
// ✓ 正确:只使用一次
|
||||
`srdb:"field:name"`
|
||||
```
|
||||
|
||||
## 性能说明
|
||||
|
||||
新 tag 格式的解析性能与旧格式相当:
|
||||
- 解析时间:< 1μs/field
|
||||
- 内存开销:无额外分配
|
||||
- 向后兼容:零性能损失
|
||||
201
examples/tag_format/main.go
Normal file
201
examples/tag_format/main.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/srdb"
|
||||
)
|
||||
|
||||
// 使用新的 tag 格式(顺序无关)
|
||||
type Product struct {
|
||||
ID uint32 `srdb:"field:id;comment:商品ID"`
|
||||
Name string `srdb:"comment:商品名称;field:name;indexed"`
|
||||
Price float64 `srdb:"field:price;nullable;comment:价格"`
|
||||
Stock int32 `srdb:"indexed;field:stock;comment:库存数量"`
|
||||
Category string `srdb:"field:category;indexed;nullable;comment:分类"`
|
||||
Description string `srdb:"nullable;field:description;comment:商品描述"`
|
||||
CreatedAt time.Time `srdb:"field:created_at;comment:创建时间"`
|
||||
UpdatedAt time.Time `srdb:"comment:更新时间;field:updated_at;nullable"`
|
||||
ExpireIn time.Duration `srdb:"field:expire_in;comment:过期时间;nullable"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== 新 Tag 格式演示 ===\n")
|
||||
|
||||
dataPath := "./tag_format_data"
|
||||
os.RemoveAll(dataPath)
|
||||
defer os.RemoveAll(dataPath)
|
||||
|
||||
// 1. 从结构体生成 Schema
|
||||
fmt.Println("1. 从结构体生成 Schema")
|
||||
fields, err := srdb.StructToFields(Product{})
|
||||
if err != nil {
|
||||
log.Fatalf("生成字段失败: %v", err)
|
||||
}
|
||||
|
||||
schema, err := srdb.NewSchema("products", fields)
|
||||
if err != nil {
|
||||
log.Fatalf("创建 Schema 失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Schema 字段:")
|
||||
for _, f := range schema.Fields {
|
||||
fmt.Printf(" - %s (%s)", f.Name, f.Type)
|
||||
if f.Indexed {
|
||||
fmt.Print(" [索引]")
|
||||
}
|
||||
if f.Nullable {
|
||||
fmt.Print(" [可空]")
|
||||
}
|
||||
if f.Comment != "" {
|
||||
fmt.Printf(" // %s", f.Comment)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// 2. 创建数据库和表
|
||||
fmt.Println("\n2. 创建数据库和表")
|
||||
db, err := srdb.Open(dataPath)
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
table, err := db.CreateTable("products", schema)
|
||||
if err != nil {
|
||||
log.Fatalf("创建表失败: %v", err)
|
||||
}
|
||||
fmt.Println(" ✓ 表创建成功")
|
||||
|
||||
// 3. 检查自动创建的索引
|
||||
fmt.Println("\n3. 检查自动创建的索引")
|
||||
indexes := table.ListIndexes()
|
||||
fmt.Printf(" 索引列表: %v\n", indexes)
|
||||
if len(indexes) == 3 {
|
||||
fmt.Println(" ✓ 自动为 name, stock, category 创建了索引")
|
||||
}
|
||||
|
||||
// 4. 插入测试数据
|
||||
fmt.Println("\n4. 插入测试数据")
|
||||
now := time.Now()
|
||||
testData := []map[string]any{
|
||||
{
|
||||
"id": uint32(1001),
|
||||
"name": "苹果 iPhone 15",
|
||||
"price": 6999.0,
|
||||
"stock": int32(50),
|
||||
"category": "电子产品",
|
||||
"description": "最新款智能手机",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"expire_in": 24 * time.Hour * 365, // 1年保修
|
||||
},
|
||||
{
|
||||
"id": uint32(1002),
|
||||
"name": "联想笔记本",
|
||||
"price": nil, // 价格待定(Nullable)
|
||||
"stock": int32(0),
|
||||
"category": "电子产品",
|
||||
"created_at": now.Add(-24 * time.Hour),
|
||||
"expire_in": 24 * time.Hour * 365 * 2, // 2年保修
|
||||
},
|
||||
{
|
||||
"id": uint32(1003),
|
||||
"name": "办公椅",
|
||||
"price": 899.0,
|
||||
"stock": int32(100),
|
||||
"category": "家具",
|
||||
"description": "人体工学设计",
|
||||
"created_at": now.Add(-48 * time.Hour),
|
||||
"expire_in": 24 * time.Hour * 365 * 5, // 5年质保
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
err := table.Insert(data)
|
||||
if err != nil {
|
||||
log.Fatalf("插入数据失败: %v", err)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" ✓ 已插入 %d 条数据\n", len(testData))
|
||||
|
||||
// 5. 使用索引查询
|
||||
fmt.Println("\n5. 使用索引查询")
|
||||
|
||||
fmt.Println(" a) 查询 category = '电子产品'")
|
||||
rows, err := table.Query().Eq("category", "电子产品").Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
row := rows.Row()
|
||||
data := row.Data()
|
||||
fmt.Printf(" - %s (ID: %d, 库存: %d)\n", data["name"], data["id"], data["stock"])
|
||||
count++
|
||||
}
|
||||
fmt.Printf(" ✓ 找到 %d 条记录\n", count)
|
||||
|
||||
fmt.Println("\n b) 查询 stock = 0 (缺货)")
|
||||
rows2, err := table.Query().Eq("stock", int32(0)).Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows2.Close()
|
||||
|
||||
for rows2.Next() {
|
||||
row := rows2.Row()
|
||||
data := row.Data()
|
||||
fmt.Printf(" - %s (缺货)\n", data["name"])
|
||||
}
|
||||
|
||||
// 6. 验证 Nullable 字段
|
||||
fmt.Println("\n6. 验证 Nullable 字段")
|
||||
rows3, err := table.Query().Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows3.Close()
|
||||
|
||||
for rows3.Next() {
|
||||
row := rows3.Row()
|
||||
data := row.Data()
|
||||
price := data["price"]
|
||||
if price == nil {
|
||||
fmt.Printf(" - %s: 价格待定 (NULL)\n", data["name"])
|
||||
} else {
|
||||
fmt.Printf(" - %s: ¥%.2f\n", data["name"], price)
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 验证 Time 和 Duration 类型
|
||||
fmt.Println("\n7. 验证 Time 和 Duration 类型")
|
||||
rows4, err := table.Query().Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("查询失败: %v", err)
|
||||
}
|
||||
defer rows4.Close()
|
||||
|
||||
for rows4.Next() {
|
||||
row := rows4.Row()
|
||||
data := row.Data()
|
||||
createdAt := data["created_at"].(time.Time)
|
||||
expireIn := data["expire_in"].(time.Duration)
|
||||
|
||||
fmt.Printf(" - %s:\n", data["name"])
|
||||
fmt.Printf(" 创建时间: %s\n", createdAt.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf(" 质保期: %v\n", expireIn)
|
||||
}
|
||||
|
||||
fmt.Println("\n=== 演示完成 ===")
|
||||
fmt.Println("\n✨ 新 Tag 格式特点:")
|
||||
fmt.Println(" • 使用 field:xxx、comment:xxx 等 key:value 格式")
|
||||
fmt.Println(" • 顺序无关,可以任意排列")
|
||||
fmt.Println(" • 支持 indexed、nullable 标记")
|
||||
fmt.Println(" • 完全向后兼容旧格式")
|
||||
}
|
||||
41
schema.go
41
schema.go
@@ -247,30 +247,51 @@ func StructToFields(v any) ([]Field, error) {
|
||||
comment := ""
|
||||
|
||||
if tag != "" {
|
||||
// 使用分号分隔各部分
|
||||
parts := strings.Split(tag, ";")
|
||||
// 使用分号分隔各部分,与顺序无关
|
||||
parts := strings.SplitSeq(tag, ";")
|
||||
|
||||
for idx, part := range parts {
|
||||
for part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if idx == 0 && part != "" {
|
||||
// 第一部分是字段名
|
||||
fieldName = part
|
||||
// 检查是否为 key:value 格式
|
||||
if after, ok := strings.CutPrefix(part, "field:"); ok {
|
||||
// field:字段名
|
||||
fieldName = after
|
||||
} else if after, ok := strings.CutPrefix(part, "comment:"); ok {
|
||||
// comment:注释内容
|
||||
comment = after
|
||||
} else if part == "indexed" {
|
||||
// indexed 标记
|
||||
indexed = true
|
||||
} else if part == "nullable" {
|
||||
// nullable 标记
|
||||
nullable = true
|
||||
} else if after, ok := strings.CutPrefix(part, "comment:"); ok {
|
||||
// comment:注释内容
|
||||
comment = after
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测实际类型(处理指针类型)
|
||||
actualType := field.Type
|
||||
isPointer := false
|
||||
|
||||
// 检测指针类型 (*string, *int64, etc.)
|
||||
if actualType.Kind() == reflect.Pointer {
|
||||
isPointer = true
|
||||
nullable = true // 指针类型自动推断为 nullable
|
||||
actualType = actualType.Elem()
|
||||
}
|
||||
|
||||
// 验证:如果 tag 显式标记了 nullable,字段必须是指针类型
|
||||
if nullable && !isPointer {
|
||||
return nil, fmt.Errorf("field %s: nullable tag requires pointer type (e.g., *%s instead of %s)",
|
||||
field.Name, actualType.String(), actualType.String())
|
||||
}
|
||||
|
||||
// 映射 Go 类型到 FieldType
|
||||
fieldType, err := goTypeToFieldType(field.Type)
|
||||
fieldType, err := goTypeToFieldType(actualType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("field %s: %w", field.Name, err)
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ func encodeSSTableRowBinary(row *SSTableRow, schema *Schema) ([]byte, error) {
|
||||
fieldBuf := new(bytes.Buffer)
|
||||
value, exists := row.Data[field.Name]
|
||||
|
||||
if !exists {
|
||||
// 字段不存在,写入零值
|
||||
if !exists || value == nil {
|
||||
// 字段不存在或值为 nil(nullable 字段),写入零值
|
||||
if err := writeFieldZeroValue(fieldBuf, field.Type); err != nil {
|
||||
return nil, fmt.Errorf("write zero value for field %s: %w", field.Name, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user