Files
srdb/examples/nullable/README.md
bourdon 8d750505fb 功能:添加可空字段和标签格式支持
- 实现可空字段(nullable)功能
  - 支持 *int, *string 等指针类型
  - 添加 nullable 示例程序
  - 完善可空字段验证测试
- 添加标签格式(tag format)支持
  - 支持自定义字段标签
  - 添加 tag_format 示例程序
  - 增强 Schema 标签解析能力
- 优化 Schema 和 SSTable 处理逻辑
- 添加诊断工具测试用例
2025-10-10 15:44:38 +08:00

8.0 KiB
Raw Blame History

Nullable 字段支持

概述

SRDB 通过指针类型来声明 nullable 字段:

  • 指针类型 (*string, *int32, ...) - 自动推断为 nullable
  • Tag 标记 (nullable) - 可选,仅用于指针类型(非指针类型会报错)

方式 1: 指针类型(推荐)

定义

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
}

使用

// 插入数据
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 显式标记(可选)

定义

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 会报错:

// ✗ 错误:非指针类型不能标记 nullable
type Wrong struct {
    Email string `srdb:"field:email;nullable"`  // 报错!
}

// ✓ 正确:必须是指针类型
type Correct struct {
    Email *string `srdb:"field:email;nullable"`  // ✓ 或省略 nullable
}

为什么要这样设计?

  • 保持类型系统的一致性
  • 避免 "string 类型但允许 NULL" 这种混乱的语义
  • 强制使用指针类型来表示 nullable语义更清晰

完整示例

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. 使用指针类型

// ✓ 推荐:指针类型,无需 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. 添加注释说明

type User struct {
    Email *string `srdb:"field:email;comment:邮箱(可选)"`
    Phone *string `srdb:"field:phone;comment:手机号(可选)"`
}

3. 一致性

在同一个结构体中,尽量使用统一的方式:

// ✓ 好:统一使用指针
type User struct {
    Email *string
    Phone *string
    Age   *int32
}

// ✗ 避免:混用
type User struct {
    Email *string
    Phone string `srdb:"nullable"`
}

当前限制

⚠️ 注意当前版本在二进制编码中NULL 值会被存储为零值。这意味着:

  • 0NULL 在 int 类型中无法区分
  • ""NULL 在 string 类型中无法区分
  • falseNULL 在 bool 类型中无法区分

未来改进 (v2.1): 我们计划在二进制编码格式中添加 NULL 标志位,完全区分零值和 NULL。

当前解决方案:

  • 对于整数类型,考虑使用特殊值(如 -1表示未设置
  • 对于字符串,考虑使用非空默认值
  • 或等待 v2.1 版本的完整 NULL 支持

FAQ

Q: 为什么推荐指针类型?

A: 指针类型是 Go 语言表示 nullable 的标准方式:

  • nil 天然表示 NULL
  • 类型系统原生支持
  • 无需额外学习成本
  • StructToFields 自动识别

Q: nullable tag 是必需的吗?

A: 不是。指针类型会自动推断为 nullable无需显式标记。

// 这两种写法等价
type User struct {
    Email *string                    // 自动 nullable
    Phone *string `srdb:"nullable"`  // 显式标记(冗余)
}

Q: 非指针类型可以标记 nullable 吗?

A: 不可以!非指针类型标记 nullable 会报错:

// ✗ 错误
type User struct {
    Email string `srdb:"nullable"`  // 报错!
}

// ✓ 正确
type User struct {
    Email *string  // 或 *string `srdb:"nullable"`
}

原因:保持类型系统的一致性,避免混乱的语义。

Q: 插入时可以省略 nullable 字段吗?

A: 可以。如果 map 中不包含某个 nullable 字段,会被视为 NULL。

// 这两种写法等价
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 和零值。


运行示例

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>