功能:添加可空字段和标签格式支持

- 实现可空字段(nullable)功能
  - 支持 *int, *string 等指针类型
  - 添加 nullable 示例程序
  - 完善可空字段验证测试
- 添加标签格式(tag format)支持
  - 支持自定义字段标签
  - 添加 tag_format 示例程序
  - 增强 Schema 标签解析能力
- 优化 Schema 和 SSTable 处理逻辑
- 添加诊断工具测试用例
This commit is contained in:
2025-10-10 14:00:34 +08:00
parent fc1ad9832d
commit 8d750505fb
6 changed files with 1131 additions and 12 deletions

353
examples/nullable/README.md Normal file
View 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
View 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 习惯")
}