重构:清理项目结构和完善文档

- 添加完整的 DOCS.md 文档(1376 行)
- 更新 README.md,增强项目说明
- 清理临时示例和测试数据
- 删除诊断工具(已完成测试)
- 为 webui 示例准备测试数据
- 优化 .gitignore 配置
- 增强 Query 和 Schema 功能
- 改进 SSTable 编码处理
This commit is contained in:
2025-10-10 16:38:19 +08:00
parent 8d750505fb
commit 7d2bb4745c
76 changed files with 1865 additions and 3587 deletions

5
.gitignore vendored
View File

@@ -36,11 +36,12 @@ testdb/
*.sst *.sst
# Example binaries # Example binaries
/examples/webui/data/ /examples/*/data/
# AI markdown # AI markdown
/*.md /*.md
!/CLAUDE.md !/CLAUDE.md
!/DESIGN.md !/DESIGN.md
!/README.md !/DOCS.md
!/LICENSE.md !/LICENSE.md
!/README.md

View File

@@ -142,13 +142,14 @@ table, _ := db.CreateTable("users", schema)
- Schema `Insert()` 时强制验证类型和必填字段 - Schema `Insert()` 时强制验证类型和必填字段
- 索引字段`Indexed: true`自动创建二级索引 - 索引字段`Indexed: true`自动创建二级索引
- Schema 持久化到 `table_dir/schema.json`包含校验和防篡改 - Schema 持久化到 `table_dir/schema.json`包含校验和防篡改
- **支持的类型** (17 精确映射到 Go 基础类型): - **支持的类型** (21 精确映射到 Go 基础类型):
- **有符号整数** (5种): `Int`, `Int8`, `Int16`, `Int32`, `Int64` - **有符号整数** (5种): `Int`, `Int8`, `Int16`, `Int32`, `Int64`
- **无符号整数** (5种): `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64` - **无符号整数** (5种): `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
- **浮点数** (2种): `Float32`, `Float64` - **浮点数** (2种): `Float32`, `Float64`
- **字符串** (1种): `String` - **字符串** (1种): `String`
- **布尔** (1种): `Bool` - **布尔** (1种): `Bool`
- **特殊类型** (3): `Byte` (独立类型底层=uint8), `Rune` (独立类型底层=int32), `Decimal` (高精度十进制使用 shopspring/decimal) - **特殊类型** (5): `Byte` (独立类型底层=uint8), `Rune` (独立类型底层=int32), `Decimal` (高精度十进制使用 shopspring/decimal), `Time` (time.Time), `Duration` (time.Duration)
- **复杂类型** (2种): `Object` (map[string]xxxstruct{}、*struct{}使用 JSON 编码), `Array` ([]xxx 切片使用 JSON 编码)
- **Nullable 支持**: 字段可标记为 `Nullable: true`允许 NULL - **Nullable 支持**: 字段可标记为 `Nullable: true`允许 NULL
### 类型系统详解 ### 类型系统详解

1376
DOCS.md Normal file

File diff suppressed because it is too large Load Diff

185
README.md
View File

@@ -13,7 +13,8 @@
- **WAL 持久化** - 写前日志保证数据安全 - **WAL 持久化** - 写前日志保证数据安全
- **自动 Compaction** - 智能的多层级数据合并策略 - **自动 Compaction** - 智能的多层级数据合并策略
- **索引支持** - 快速的字段查询能力 - **索引支持** - 快速的字段查询能力
- **Schema 管理** - 灵活的表结构定义 - **Schema 管理** - 灵活的表结构定义,支持 21 种类型
- **复杂类型** - 原生支持 Objectmap和 Arrayslice
### 查询能力 ### 查询能力
- **链式查询 API** - 流畅的查询构建器 - **链式查询 API** - 流畅的查询构建器
@@ -21,6 +22,7 @@
- **复合条件** - `AND`, `OR`, `NOT` 逻辑组合 - **复合条件** - `AND`, `OR`, `NOT` 逻辑组合
- **字段选择** - 按需加载指定字段,优化性能 - **字段选择** - 按需加载指定字段,优化性能
- **游标模式** - 惰性加载,支持大数据集遍历 - **游标模式** - 惰性加载,支持大数据集遍历
- **智能 Scan** - 自动扫描到结构体,完整支持复杂类型
### 管理工具 ### 管理工具
- **Web UI** - 现代化的数据库管理界面 - **Web UI** - 现代化的数据库管理界面
@@ -34,10 +36,13 @@
- [快速开始](#快速开始) - [快速开始](#快速开始)
- [基本用法](#基本用法) - [基本用法](#基本用法)
- [查询 API](#查询-api) - [查询 API](#查询-api)
- [Scan 方法](#scan-方法---扫描到结构体)
- [Object 和 Array 类型](#object-和-array-类型)
- [Web UI](#web-ui) - [Web UI](#web-ui)
- [架构设计](#架构设计) - [架构设计](#架构设计)
- [性能特点](#性能特点) - [性能特点](#性能特点)
- [开发指南](#开发指南) - [开发指南](#开发指南)
- [文档](#文档)
--- ---
@@ -69,12 +74,15 @@ func main() {
defer db.Close() defer db.Close()
// 2. 定义 Schema // 2. 定义 Schema
schema := srdb.NewSchema("users", []srdb.Field{ schema, err := srdb.NewSchema("users", []srdb.Field{
{Name: "id", Type: srdb.FieldTypeInt64, Indexed: true, Comment: "用户ID"}, {Name: "id", Type: srdb.Int64, Indexed: true, Comment: "用户ID"},
{Name: "name", Type: srdb.FieldTypeString, Indexed: false, Comment: "用户名"}, {Name: "name", Type: srdb.String, Indexed: false, Comment: "用户名"},
{Name: "email", Type: srdb.FieldTypeString, Indexed: true, Comment: "邮箱"}, {Name: "email", Type: srdb.String, Indexed: true, Comment: "邮箱"},
{Name: "age", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "年龄"}, {Name: "age", Type: srdb.Int32, Indexed: false, Comment: "年龄"},
}) })
if err != nil {
log.Fatal(err)
}
// 3. 创建表 // 3. 创建表
table, err := db.CreateTable("users", schema) table, err := db.CreateTable("users", schema)
@@ -158,33 +166,64 @@ err := table.Update(seq, map[string]any{
### Schema 定义 ### Schema 定义
```go ```go
schema := srdb.NewSchema("logs", []srdb.Field{ schema, err := srdb.NewSchema("logs", []srdb.Field{
{ {
Name: "group", Name: "level",
Type: srdb.FieldTypeString, Type: srdb.String,
Indexed: true, Indexed: true,
Comment: "日志分组", Comment: "日志级别",
}, },
{ {
Name: "message", Name: "message",
Type: srdb.FieldTypeString, Type: srdb.String,
Indexed: false, Indexed: false,
Comment: "日志内容", Comment: "日志内容",
}, },
{ {
Name: "timestamp", Name: "timestamp",
Type: srdb.FieldTypeInt64, Type: srdb.Int64,
Indexed: true, Indexed: true,
Comment: "时间戳", Comment: "时间戳",
}, },
{
Name: "metadata",
Type: srdb.Object,
Indexed: false,
Comment: "元数据map",
},
{
Name: "tags",
Type: srdb.Array,
Indexed: false,
Comment: "标签slice",
},
}) })
``` ```
**支持的字段类型** **支持的字段类型**21 种)
- `FieldTypeString` - 字符串
- `FieldTypeInt64` - 64位整数 **有符号整数**
- `FieldTypeBool` - 布尔值 - `Int`, `Int8`, `Int16`, `Int32`, `Int64`
- `FieldTypeFloat64` - 64位浮点数
**无符号整数**
- `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
**浮点数**
- `Float32`, `Float64`
**基础类型**
- `String` - 字符串
- `Bool` - 布尔值
- `Byte` - 字节uint8
- `Rune` - 字符int32
**特殊类型**
- `Decimal` - 高精度十进制(需要 shopspring/decimal
- `Time` - 时间戳time.Time
**复杂类型**
- `Object` - 对象map[string]xxx、struct{}、*struct{}
- `Array` - 数组([]xxx 切片)
--- ---
@@ -288,12 +327,51 @@ data := rows.Collect()
// 获取总数 // 获取总数
count := rows.Count() count := rows.Count()
// 扫描到结构体
var users []User
err := rows.Scan(&users)
``` ```
### Scan 方法 - 扫描到结构体
SRDB 提供智能的 Scan 方法,完整支持 Object 和 Array 类型:
```go
// 定义结构体
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Settings map[string]string `json:"settings"` // Object 类型
Tags []string `json:"tags"` // Array 类型
}
// 扫描多行到切片
var users []User
table.Query().Scan(&users)
// 扫描单行到结构体(智能判断)
var user User
table.Query().Eq("name", "Alice").Scan(&user)
// Row.Scan - 扫描当前行
row, _ := table.Query().First()
var user User
row.Scan(&user)
// 部分字段扫描(性能优化)
type UserBrief struct {
Name string `json:"name"`
Email string `json:"email"`
}
var briefs []UserBrief
table.Query().Select("name", "email").Scan(&briefs)
```
**Scan 特性**
- ✅ 智能判断目标类型(切片 vs 结构体)
- ✅ 完整支持 Objectmap和 Arrayslice类型
- ✅ 支持嵌套结构
- ✅ 结合 Select() 优化性能
详细示例:[examples/scan_demo](examples/scan_demo/README.md)
### 完整的操作符列表 ### 完整的操作符列表
| 操作符 | 方法 | 说明 | | 操作符 | 方法 | 说明 |
@@ -317,6 +395,61 @@ err := rows.Scan(&users)
| `IS NULL` | `IsNull(field)` | 为空 | | `IS NULL` | `IsNull(field)` | 为空 |
| `IS NOT NULL` | `NotNull(field)` | 不为空 | | `IS NOT NULL` | `NotNull(field)` | 不为空 |
### Object 和 Array 类型
SRDB 支持复杂的数据类型,可以存储 JSON 风格的对象和数组:
```go
// 定义包含复杂类型的表
type Article struct {
Title string `srdb:"field:title"`
Content string `srdb:"field:content"`
Tags []string `srdb:"field:tags"` // Array 类型
Metadata map[string]any `srdb:"field:metadata"` // Object 类型
Authors []string `srdb:"field:authors"` // Array 类型
}
// 使用 StructToFields 自动生成 Schema
fields, _ := srdb.StructToFields(Article{})
schema, _ := srdb.NewSchema("articles", fields)
table, _ := db.CreateTable("articles", schema)
// 插入数据
table.Insert(map[string]any{
"title": "SRDB 使用指南",
"content": "...",
"tags": []any{"database", "golang", "lsm-tree"},
"metadata": map[string]any{
"category": "tech",
"views": 1250,
"featured": true,
},
"authors": []any{"Alice", "Bob"},
})
// 查询和扫描
var article Article
table.Query().Eq("title", "SRDB 使用指南").Scan(&article)
fmt.Println(article.Tags) // ["database", "golang", "lsm-tree"]
fmt.Println(article.Metadata["category"]) // "tech"
fmt.Println(article.Metadata["views"]) // 1250
```
**支持的场景**
-`map[string]xxx` - 任意键值对
-`struct{}` - 结构体(自动转换为 Object
-`*struct{}` - 结构体指针
-`[]xxx` - 任意类型的切片
- ✅ 嵌套的 Object 和 Array
- ✅ 空对象 `{}` 和空数组 `[]`
**存储细节**
- Object 和 Array 使用 JSON 编码存储
- 存储格式:`[length: uint32][JSON data]`
- 零值Object 为 `{}`Array 为 `[]`
- 支持任意嵌套深度
--- ---
## 🌐 Web UI ## 🌐 Web UI
@@ -497,10 +630,18 @@ go build -o webui main.go
## 📚 文档 ## 📚 文档
### 核心文档
- [设计文档](DESIGN.md) - 详细的架构设计和实现原理 - [设计文档](DESIGN.md) - 详细的架构设计和实现原理
- [WebUI 文档](examples/webui/README.md) - Web 管理界面使用指南 - [CLAUDE.md](CLAUDE.md) - 完整的开发者指南
- [Nullable 指南](NULLABLE_GUIDE.md) - Nullable 字段使用说明
- [API 文档](https://pkg.go.dev/code.tczkiot.com/wlw/srdb) - Go API 参考 - [API 文档](https://pkg.go.dev/code.tczkiot.com/wlw/srdb) - Go API 参考
### 示例和教程
- [Scan 方法指南](examples/scan_demo/README.md) - 扫描到结构体,支持 Object 和 Array
- [WebUI 工具](examples/webui/README.md) - Web 管理界面使用指南
- [所有类型示例](examples/all_types/) - 21 种类型的完整示例
- [Nullable 示例](examples/nullable/) - Nullable 字段的使用
--- ---
## 🤝 贡献 ## 🤝 贡献

View File

@@ -1,34 +0,0 @@
{
"version": 1,
"timestamp": 1760028726,
"checksum": "89e806ac5fbd5839456b425a2293097529b3edac6360f97afb06a1211d4fd53b",
"schema": {
"Name": "sensors",
"Fields": [
{
"Name": "device_id",
"Type": 9,
"Indexed": true,
"Comment": "设备ID"
},
{
"Name": "temperature",
"Type": 11,
"Indexed": false,
"Comment": "温度(摄氏度)"
},
{
"Name": "humidity",
"Type": 7,
"Indexed": false,
"Comment": "湿度0-100"
},
{
"Name": "online",
"Type": 14,
"Indexed": false,
"Comment": "是否在线"
}
]
}
}

View File

@@ -1 +0,0 @@
2

View File

@@ -1,98 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"code.tczkiot.com/wlw/srdb"
)
func main() {
fmt.Println("=== SRDB 完整类型系统示例 ===\n")
// 清理旧数据
os.RemoveAll("./data")
// 示例 1: 展示所有类型
fmt.Println("=== 示例 1: 展示所有 14 种支持的类型 ===")
showAllTypes()
// 示例 2: 实际应用场景
fmt.Println("\n=== 示例 2: 实际应用 - 物联网传感器数据 ===")
sensorDataExample()
fmt.Println("\n✓ 所有示例执行成功!")
}
func showAllTypes() {
// 展示类型映射
types := []struct {
name string
goType string
srdbType srdb.FieldType
}{
{"有符号整数", "int", srdb.Int},
{"8位有符号整数", "int8", srdb.Int8},
{"16位有符号整数", "int16", srdb.Int16},
{"32位有符号整数", "int32", srdb.Int32},
{"64位有符号整数", "int64", srdb.Int64},
{"无符号整数", "uint", srdb.Uint},
{"8位无符号整数", "uint8 (byte)", srdb.Uint8},
{"16位无符号整数", "uint16", srdb.Uint16},
{"32位无符号整数", "uint32", srdb.Uint32},
{"64位无符号整数", "uint64", srdb.Uint64},
{"单精度浮点", "float32", srdb.Float32},
{"双精度浮点", "float64", srdb.Float64},
{"字符串", "string", srdb.String},
{"布尔", "bool", srdb.Bool},
}
fmt.Println("SRDB 类型系统(精确映射到 Go 基础类型):\n")
for i, t := range types {
fmt.Printf("%2d. %-20s %-20s -> %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 (语义清晰)")
}

View File

@@ -1,162 +0,0 @@
# 批量插入示例
这个示例展示了 SRDB 的批量插入功能,支持多种数据类型的插入。
## 功能特性
SRDB 的 `Insert` 方法支持以下输入类型:
1. **单个 map**: `map[string]any`
2. **map 切片**: `[]map[string]any`
3. **单个结构体**: `struct{}`
4. **结构体指针**: `*struct{}`
5. **结构体切片**: `[]struct{}`
6. **结构体指针切片**: `[]*struct{}`
## 运行示例
```bash
cd examples/batch_insert
go run main.go
```
## 示例说明
### 示例 1: 插入单个 map
```go
err = table.Insert(map[string]any{
"name": "Alice",
"age": int64(25),
})
```
最基本的插入方式,适合动态数据。
### 示例 2: 批量插入 map 切片
```go
err = table.Insert([]map[string]any{
{"name": "Alice", "age": int64(25), "email": "alice@example.com"},
{"name": "Bob", "age": int64(30), "email": "bob@example.com"},
{"name": "Charlie", "age": int64(35), "email": "charlie@example.com"},
})
```
批量插入多条数据,提高插入效率。
### 示例 3: 插入单个结构体
```go
type User struct {
Name string `srdb:"name;comment:用户名"`
Age int64 `srdb:"age;comment:年龄"`
Email string `srdb:"email;indexed;comment:邮箱"`
IsActive bool `srdb:"is_active;comment:是否激活"`
}
user := User{
Name: "Alice",
Age: 25,
Email: "alice@example.com",
IsActive: true,
}
err = table.Insert(user)
```
使用结构体插入,提供类型安全和代码可读性。
### 示例 4: 批量插入结构体切片
```go
users := []User{
{Name: "Alice", Age: 25, Email: "alice@example.com", IsActive: true},
{Name: "Bob", Age: 30, Email: "bob@example.com", IsActive: true},
{Name: "Charlie", Age: 35, Email: "charlie@example.com", IsActive: false},
}
err = table.Insert(users)
```
批量插入结构体,适合需要插入大量数据的场景。
### 示例 5: 批量插入结构体指针切片
```go
users := []*User{
{Name: "Alice", Age: 25, Email: "alice@example.com", IsActive: true},
{Name: "Bob", Age: 30, Email: "bob@example.com", IsActive: true},
nil, // nil 指针会被自动跳过
{Name: "Charlie", Age: 35, Email: "charlie@example.com", IsActive: false},
}
err = table.Insert(users)
```
支持指针切片nil 指针会被自动跳过。
### 示例 6: 使用 snake_case 自动转换
```go
type Product struct {
ProductID string `srdb:";comment:产品ID"` // 自动转为 product_id
ProductName string `srdb:";comment:产品名称"` // 自动转为 product_name
Price float64 `srdb:";comment:价格"` // 自动转为 price
InStock bool `srdb:";comment:是否有货"` // 自动转为 in_stock
}
products := []Product{
{ProductID: "P001", ProductName: "Laptop", Price: 999.99, InStock: true},
{ProductID: "P002", ProductName: "Mouse", Price: 29.99, InStock: true},
}
err = table.Insert(products)
```
不指定字段名时,会自动将驼峰命名转换为 snake_case
- `ProductID``product_id`
- `ProductName``product_name`
- `InStock``in_stock`
## Struct Tag 格式
```go
type User struct {
// 完整格式:字段名;索引;注释
Email string `srdb:"email;indexed;comment:邮箱地址"`
// 使用默认字段名snake_case+ 注释
UserName string `srdb:";comment:用户名"` // 自动转为 user_name
// 不使用 tag完全依赖 snake_case 转换
PhoneNumber string // 自动转为 phone_number
// 忽略字段
Internal string `srdb:"-"`
}
```
## 性能优化
批量插入相比逐条插入:
- ✅ 减少函数调用开销
- ✅ 统一类型转换和验证
- ✅ 更清晰的代码逻辑
- ✅ 适合大批量数据导入
## 注意事项
1. **类型匹配**: 确保结构体字段类型与 Schema 定义一致
2. **Schema 验证**: 所有数据都会经过 Schema 验证
3. **nil 处理**: 结构体指针切片中的 nil 会被自动跳过
4. **字段名转换**: 未指定 tag 时自动使用 snake_case 转换
5. **索引更新**: 带索引的字段会自动更新索引
## 相关文档
- [STRUCT_TAG_GUIDE.md](../../STRUCT_TAG_GUIDE.md) - Struct Tag 完整指南
- [SNAKE_CASE_CONVERSION.md](../../SNAKE_CASE_CONVERSION.md) - snake_case 转换规则
- [examples/struct_schema](../struct_schema) - 结构体 Schema 示例

View File

@@ -1,22 +0,0 @@
{
"version": 1,
"timestamp": 1760013696,
"checksum": "343b1f86cfc4ed9471b71b3a63d61b4205b17cf953f4a2698f2d3ebd37540caa",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "name",
"Type": 2,
"Indexed": false,
"Comment": "用户名"
},
{
"Name": "age",
"Type": 1,
"Indexed": false,
"Comment": "年龄"
}
]
}
}

View File

@@ -1,28 +0,0 @@
{
"version": 1,
"timestamp": 1760013696,
"checksum": "961d723306aa507e4599ad24afe32e0bc30dcac5f9d1aabd1a18128376767a36",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "name",
"Type": 2,
"Indexed": false,
"Comment": ""
},
{
"Name": "age",
"Type": 1,
"Indexed": false,
"Comment": ""
},
{
"Name": "email",
"Type": 2,
"Indexed": true,
"Comment": ""
}
]
}
}

View File

@@ -1 +0,0 @@
MANIFEST-000001

View File

@@ -1,34 +0,0 @@
{
"version": 1,
"timestamp": 1760013696,
"checksum": "dab23ff2118dc2a790681dcc4e112b3377fd96414a30cdfa0ca98af5d41a271e",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "name",
"Type": 2,
"Indexed": false,
"Comment": ""
},
{
"Name": "age",
"Type": 1,
"Indexed": false,
"Comment": ""
},
{
"Name": "email",
"Type": 2,
"Indexed": false,
"Comment": ""
},
{
"Name": "is_active",
"Type": 4,
"Indexed": false,
"Comment": ""
}
]
}
}

View File

@@ -1 +0,0 @@
MANIFEST-000001

View File

@@ -1,34 +0,0 @@
{
"version": 1,
"timestamp": 1760013696,
"checksum": "dab23ff2118dc2a790681dcc4e112b3377fd96414a30cdfa0ca98af5d41a271e",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "name",
"Type": 2,
"Indexed": false,
"Comment": ""
},
{
"Name": "age",
"Type": 1,
"Indexed": false,
"Comment": ""
},
{
"Name": "email",
"Type": 2,
"Indexed": false,
"Comment": ""
},
{
"Name": "is_active",
"Type": 4,
"Indexed": false,
"Comment": ""
}
]
}
}

View File

@@ -1 +0,0 @@
MANIFEST-000001

View File

@@ -1,34 +0,0 @@
{
"version": 1,
"timestamp": 1760013696,
"checksum": "dab23ff2118dc2a790681dcc4e112b3377fd96414a30cdfa0ca98af5d41a271e",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "name",
"Type": 2,
"Indexed": false,
"Comment": ""
},
{
"Name": "age",
"Type": 1,
"Indexed": false,
"Comment": ""
},
{
"Name": "email",
"Type": 2,
"Indexed": false,
"Comment": ""
},
{
"Name": "is_active",
"Type": 4,
"Indexed": false,
"Comment": ""
}
]
}
}

View File

@@ -1 +0,0 @@
MANIFEST-000001

View File

@@ -1,34 +0,0 @@
{
"version": 1,
"timestamp": 1760013696,
"checksum": "36398ae9f0d772f19a24dd8562fde01665890335416e4951808c6b050d251b43",
"schema": {
"Name": "products",
"Fields": [
{
"Name": "product_id",
"Type": 2,
"Indexed": false,
"Comment": "产品ID"
},
{
"Name": "product_name",
"Type": 2,
"Indexed": false,
"Comment": "产品名称"
},
{
"Name": "price",
"Type": 3,
"Indexed": false,
"Comment": "价格"
},
{
"Name": "in_stock",
"Type": 4,
"Indexed": false,
"Comment": "是否有货"
}
]
}
}

View File

@@ -1,321 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"time"
"code.tczkiot.com/wlw/srdb"
)
// User 用户结构体
type User struct {
Name string `srdb:"name;comment:用户名"`
Age int64 `srdb:"age;comment:年龄"`
Email string `srdb:"email;indexed;comment:邮箱"`
IsActive bool `srdb:"is_active;comment:是否激活"`
}
// Product 产品结构体(使用默认 snake_case 转换)
type Product struct {
ProductID string `srdb:";comment:产品ID"` // 自动转为 product_id
ProductName string `srdb:";comment:产品名称"` // 自动转为 product_name
Price float64 `srdb:";comment:价格"` // 自动转为 price
InStock bool `srdb:";comment:是否有货"` // 自动转为 in_stock
}
func main() {
fmt.Println("=== SRDB 批量插入示例 ===")
// 清理旧数据
os.RemoveAll("./data")
// 示例 1: 插入单个 map
example1()
// 示例 2: 批量插入 map 切片
example2()
// 示例 3: 插入单个结构体
example3()
// 示例 4: 批量插入结构体切片
example4()
// 示例 5: 批量插入结构体指针切片
example5()
// 示例 6: 使用 snake_case 自动转换
example6()
fmt.Println("\n✓ 所有示例执行成功!")
}
func example1() {
fmt.Println("=== 示例 1: 插入单个 map ===")
schema, err := srdb.NewSchema("users", []srdb.Field{
{Name: "name", Type: srdb.String, Comment: "用户名"},
{Name: "age", Type: srdb.Int64, Comment: "年龄"},
})
if err != nil {
log.Fatal(err)
}
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/example1",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 插入单条数据
err = table.Insert(map[string]any{
"name": "Alice",
"age": int64(25),
})
if err != nil {
log.Fatal(err)
}
fmt.Println("✓ 插入 1 条数据")
// 查询
row, _ := table.Get(1)
fmt.Printf(" 查询结果: name=%s, age=%d\n\n", row.Data["name"], row.Data["age"])
}
func example2() {
fmt.Println("=== 示例 2: 批量插入 map 切片 ===")
schema, err := srdb.NewSchema("users", []srdb.Field{
{Name: "name", Type: srdb.String},
{Name: "age", Type: srdb.Int64},
{Name: "email", Type: srdb.String, Indexed: true},
})
if err != nil {
log.Fatal(err)
}
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/example2",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 批量插入
start := time.Now()
err = table.Insert([]map[string]any{
{"name": "Alice", "age": int64(25), "email": "alice@example.com"},
{"name": "Bob", "age": int64(30), "email": "bob@example.com"},
{"name": "Charlie", "age": int64(35), "email": "charlie@example.com"},
{"name": "David", "age": int64(40), "email": "david@example.com"},
{"name": "Eve", "age": int64(45), "email": "eve@example.com"},
})
if err != nil {
log.Fatal(err)
}
elapsed := time.Since(start)
fmt.Printf("✓ 批量插入 5 条数据,耗时: %v\n", elapsed)
// 使用索引查询
rows, _ := table.Query().Eq("email", "bob@example.com").Rows()
defer rows.Close()
if rows.Next() {
row := rows.Row()
data := row.Data()
fmt.Printf(" 索引查询结果: name=%s, email=%s\n\n", data["name"], data["email"])
}
}
func example3() {
fmt.Println("=== 示例 3: 插入单个结构体 ===")
schema, err := srdb.NewSchema("users", []srdb.Field{
{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)
}
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/example3",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 插入结构体
user := User{
Name: "Alice",
Age: 25,
Email: "alice@example.com",
IsActive: true,
}
err = table.Insert(user)
if err != nil {
log.Fatal(err)
}
fmt.Println("✓ 插入 1 个结构体")
// 查询
row, _ := table.Get(1)
fmt.Printf(" 查询结果: name=%s, age=%d, active=%v\n\n",
row.Data["name"], row.Data["age"], row.Data["is_active"])
}
func example4() {
fmt.Println("=== 示例 4: 批量插入结构体切片 ===")
schema, err := srdb.NewSchema("users", []srdb.Field{
{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)
}
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/example4",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 批量插入结构体切片
users := []User{
{Name: "Alice", Age: 25, Email: "alice@example.com", IsActive: true},
{Name: "Bob", Age: 30, Email: "bob@example.com", IsActive: true},
{Name: "Charlie", Age: 35, Email: "charlie@example.com", IsActive: false},
}
start := time.Now()
err = table.Insert(users)
if err != nil {
log.Fatal(err)
}
elapsed := time.Since(start)
fmt.Printf("✓ 批量插入 %d 个结构体,耗时: %v\n", len(users), elapsed)
// 查询所有激活用户
rows, _ := table.Query().Eq("is_active", true).Rows()
defer rows.Close()
count := 0
for rows.Next() {
count++
}
fmt.Printf(" 查询结果: 找到 %d 个激活用户\n\n", count)
}
func example5() {
fmt.Println("=== 示例 5: 批量插入结构体指针切片 ===")
schema, err := srdb.NewSchema("users", []srdb.Field{
{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)
}
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/example5",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 批量插入结构体指针切片
users := []*User{
{Name: "Alice", Age: 25, Email: "alice@example.com", IsActive: true},
{Name: "Bob", Age: 30, Email: "bob@example.com", IsActive: true},
nil, // nil 指针会被自动跳过
{Name: "Charlie", Age: 35, Email: "charlie@example.com", IsActive: false},
}
err = table.Insert(users)
if err != nil {
log.Fatal(err)
}
fmt.Printf("✓ 批量插入 %d 个结构体指针nil 自动跳过)\n", len(users))
// 验证插入数量
row, _ := table.Get(3)
fmt.Printf(" 实际插入: 3 条数据, 最后一条 name=%s\n\n", row.Data["name"])
}
func example6() {
fmt.Println("=== 示例 6: 使用 snake_case 自动转换 ===")
schema, err := srdb.NewSchema("products", []srdb.Field{
{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)
}
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/example6",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 结构体字段名是驼峰命名,会自动转为 snake_case
products := []Product{
{ProductID: "P001", ProductName: "Laptop", Price: 999.99, InStock: true},
{ProductID: "P002", ProductName: "Mouse", Price: 29.99, InStock: true},
{ProductID: "P003", ProductName: "Keyboard", Price: 79.99, InStock: false},
}
err = table.Insert(products)
if err != nil {
log.Fatal(err)
}
fmt.Printf("✓ 批量插入 %d 个产品(自动 snake_case 转换)\n", len(products))
// 查询
row, _ := table.Get(1)
fmt.Printf(" 字段名自动转换:\n")
fmt.Printf(" ProductID -> product_id = %s\n", row.Data["product_id"])
fmt.Printf(" ProductName -> product_name = %s\n", row.Data["product_name"])
fmt.Printf(" Price -> price = %.2f\n", row.Data["price"])
fmt.Printf(" InStock -> in_stock = %v\n\n", row.Data["in_stock"])
}

View File

@@ -1,129 +0,0 @@
# 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 类型保证金融数据精确性

View File

@@ -1 +0,0 @@
MANIFEST-000001

View File

@@ -1,31 +0,0 @@
{
"version": 1,
"timestamp": 1760030838,
"checksum": "db36b32950014d2d1551c398e67a7c463fd04bdf98be5a9dec64675cdb0882af",
"schema": {
"Name": "api_logs",
"Fields": [
{
"Name": "endpoint",
"Type": 13,
"Indexed": false,
"Nullable": false,
"Comment": "API 端点"
},
{
"Name": "status_code",
"Type": 15,
"Indexed": false,
"Nullable": false,
"Comment": "HTTP 状态码(用 byte 节省空间)"
},
{
"Name": "response_time_ms",
"Type": 8,
"Indexed": false,
"Nullable": false,
"Comment": "响应时间(毫秒)"
}
]
}
}

View File

@@ -1 +0,0 @@
2

View File

@@ -1,15 +0,0 @@
module code.tczkiot.com/wlw/srdb/examples/new_types
go 1.24.0
replace code.tczkiot.com/wlw/srdb => ../..
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
)

View File

@@ -1,6 +0,0 @@
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=

View File

@@ -1,378 +0,0 @@
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())
}

View File

@@ -1,353 +0,0 @@
# 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>
```

View File

@@ -1,289 +0,0 @@
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 习惯")
}

View File

@@ -1,101 +0,0 @@
package main
import (
"fmt"
"log"
"code.tczkiot.com/wlw/srdb"
)
// 演示各种驼峰命名自动转换为 snake_case
type DemoStruct struct {
// 基本转换
UserName string `srdb:";comment:用户名"` // -> user_name
EmailAddress string `srdb:";comment:邮箱地址"` // -> email_address
PhoneNumber string `srdb:";comment:手机号"` // -> phone_number
// 连续大写字母
HTTPEndpoint string `srdb:";comment:HTTP 端点"` // -> http_endpoint
URLPath string `srdb:";comment:URL 路径"` // -> url_path
XMLParser string `srdb:";comment:XML 解析器"` // -> xml_parser
// 短命名
ID int64 `srdb:";comment:ID"` // -> id
// 布尔值
IsActive bool `srdb:";comment:是否激活"` // -> is_active
IsDeleted bool `srdb:";comment:是否删除"` // -> is_deleted
// 数字混合
Address1 string `srdb:";comment:地址1"` // -> address1
User2Name string `srdb:";comment:用户2名称"` // -> user2_name
}
func main() {
fmt.Println("=== snake_case 自动转换演示 ===")
// 生成 Field 列表
fields, err := srdb.StructToFields(DemoStruct{})
if err != nil {
log.Fatal(err)
}
// 打印转换结果
fmt.Println("\n字段名转换驼峰命名 -> snake_case")
fmt.Printf("%-20s -> %-20s %s\n", "Go 字段名", "数据库字段名", "注释")
fmt.Println(string(make([]byte, 70)) + "\n" + string(make([]byte, 70)))
type fieldInfo struct {
goName string
dbName string
comment string
}
fieldMapping := []fieldInfo{
{"UserName", "user_name", "用户名"},
{"EmailAddress", "email_address", "邮箱地址"},
{"PhoneNumber", "phone_number", "手机号"},
{"HTTPEndpoint", "http_endpoint", "HTTP 端点"},
{"URLPath", "url_path", "URL 路径"},
{"XMLParser", "xml_parser", "XML 解析器"},
{"ID", "id", "ID"},
{"IsActive", "is_active", "是否激活"},
{"IsDeleted", "is_deleted", "是否删除"},
{"Address1", "address1", "地址1"},
{"User2Name", "user2_name", "用户2名称"},
}
for i, field := range fields {
if i < len(fieldMapping) {
fmt.Printf("%-20s -> %-20s %s\n",
fieldMapping[i].goName,
field.Name,
field.Comment)
}
}
// 验证转换
fmt.Println("\n=== 转换验证 ===")
allCorrect := true
for i, field := range fields {
if i < len(fieldMapping) {
expected := fieldMapping[i].dbName
if field.Name != expected {
fmt.Printf("❌ %s: 期望 %s, 实际 %s\n",
fieldMapping[i].goName, expected, field.Name)
allCorrect = false
}
}
}
if allCorrect {
fmt.Println("✅ 所有字段名转换正确!")
}
// 创建 Schema
schema, err := srdb.NewSchema("demo", fields)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\n✅ 成功创建 Schema包含 %d 个字段\n", len(schema.Fields))
}

View File

@@ -1,153 +0,0 @@
# StructToFields 示例
这个示例展示如何使用 `StructToFields` 方法从 Go 结构体自动生成 Schema。
## 功能特性
- ✅ 从结构体自动生成 Field 列表
- ✅ 支持 struct tags 定义字段属性
- ✅ 支持索引标记
- ✅ 支持字段注释
- ✅ 自动类型映射
- ✅ 支持忽略字段
## Struct Tag 格式
### srdb tag
所有配置都在 `srdb` tag 中,使用分号 `;` 分隔:
```go
type User struct {
// 基本用法:指定字段名
Name string `srdb:"name"`
// 标记为索引字段
Email string `srdb:"email;indexed"`
// 完整格式:字段名;索引;注释
Age int64 `srdb:"age;comment:年龄"`
// 带索引和注释
Phone string `srdb:"phone;indexed;comment:手机号"`
// 忽略该字段
Internal string `srdb:"-"`
// 不使用 tag默认使用 snake_case 转换
Score float64 // 字段名: score
UserID string // 字段名: user_id
}
```
### Tag 格式说明
格式:`srdb:"字段名;选项1;选项2;..."`
- **字段名**(第一部分):指定数据库中的字段名,省略则自动将结构体字段名转为 snake_case
- **indexed**:标记该字段需要建立索引
- **comment:注释内容**:字段注释说明
### 默认字段名转换snake_case
如果不指定字段名,会自动将驼峰命名转换为 snake_case
- `UserName``user_name`
- `EmailAddress``email_address`
- `IsActive``is_active`
- `HTTPServer``http_server`
- `ID``id`
## 类型映射
| Go 类型 | FieldType |
|---------|-----------|
| int, int64, int32, int16, int8 | FieldTypeInt64 |
| uint, uint64, uint32, uint16, uint8 | FieldTypeInt64 |
| string | FieldTypeString |
| float64, float32 | FieldTypeFloat |
| bool | FieldTypeBool |
## 完整示例
```go
package main
import (
"log"
"code.tczkiot.com/wlw/srdb"
)
// 定义结构体
type User struct {
Name string `srdb:"name;indexed;comment:用户名"`
Age int64 `srdb:"age;comment:年龄"`
Email string `srdb:"email;indexed;comment:邮箱"`
Score float64 `srdb:"score;comment:分数"`
IsActive bool `srdb:"is_active;comment:是否激活"`
}
func main() {
// 1. 从结构体生成 Field 列表
fields, err := srdb.StructToFields(User{})
if err != nil {
log.Fatal(err)
}
// 2. 创建 Schema
schema := srdb.NewSchema("users", fields)
// 3. 创建表
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/users",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 4. 插入数据
err = table.Insert(map[string]any{
"name": "张三",
"age": int64(25),
"email": "zhangsan@example.com",
"score": 95.5,
"is_active": true,
})
// 5. 查询数据(自动使用索引)
rows, _ := table.Query().Eq("email", "zhangsan@example.com").Rows()
defer rows.Close()
for rows.Next() {
data := rows.Row().Data()
// 处理数据...
}
}
```
## 运行示例
```bash
cd examples/struct_schema
go run main.go
```
## 优势
1. **类型安全**: 使用结构体定义,编译时检查类型
2. **简洁**: 不需要手动创建 Field 列表
3. **可维护**: 结构体和 Schema 在一起,便于维护
4. **灵活**: 支持 tag 自定义字段属性
5. **自动索引**: 通过 `indexed` tag 自动创建索引
## 注意事项
1. 只有导出的字段(首字母大写)会被包含
2. 使用 `srdb:"-"` 可以忽略字段
3. 如果不指定字段名,默认使用小写的字段名
4. 不支持嵌套结构体(需要手动展开)
5. 不支持切片、map 等复杂类型

View File

@@ -1 +0,0 @@
MANIFEST-000001

View File

@@ -1,40 +0,0 @@
{
"version": 1,
"timestamp": 1760013049,
"checksum": "fad0aaaa6fc5b94364c0c0b07a7567f71e8bb4f8ed8456c1954cffa7538a6a42",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "name",
"Type": 2,
"Indexed": true,
"Comment": "用户名"
},
{
"Name": "age",
"Type": 1,
"Indexed": false,
"Comment": "年龄"
},
{
"Name": "email",
"Type": 2,
"Indexed": true,
"Comment": "邮箱"
},
{
"Name": "score",
"Type": 3,
"Indexed": false,
"Comment": "分数"
},
{
"Name": "is_active",
"Type": 4,
"Indexed": false,
"Comment": "是否激活"
}
]
}
}

View File

@@ -1 +0,0 @@
3

View File

@@ -1,133 +0,0 @@
package main
import (
"fmt"
"log"
"code.tczkiot.com/wlw/srdb"
)
// User 用户结构体
// 使用 struct tags 定义 Schema
type User struct {
Name string `srdb:"name;indexed;comment:用户名"`
Age int64 `srdb:"age;comment:年龄"`
Email string `srdb:"email;indexed;comment:邮箱"`
Score float64 `srdb:"score;comment:分数"`
IsActive bool `srdb:"is_active;comment:是否激活"`
Internal string `srdb:"-"` // 不会被包含在 Schema 中
}
// Product 产品结构体
// 不使用 srdb tag字段名会自动转为 snake_case
type Product struct {
ProductID string // 字段名: product_id
ProductName string // 字段名: product_name
Price int64 // 字段名: price
InStock bool // 字段名: in_stock
}
func main() {
// 示例 1: 使用结构体创建 Schema
fmt.Println("=== 示例 1: 从结构体创建 Schema ===")
// 从 User 结构体生成 Field 列表
fields, err := srdb.StructToFields(User{})
if err != nil {
log.Fatal(err)
}
// 创建 Schema
schema, err := srdb.NewSchema("users", fields)
if err != nil {
log.Fatal(err)
}
// 打印 Schema 信息
fmt.Printf("Schema 名称: %s\n", schema.Name)
fmt.Printf("字段数量: %d\n", len(schema.Fields))
fmt.Println("\n字段列表:")
for _, field := range schema.Fields {
indexed := ""
if field.Indexed {
indexed = " [索引]"
}
fmt.Printf(" - %s (%s)%s: %s\n",
field.Name, field.Type.String(), indexed, field.Comment)
}
// 示例 2: 使用 Schema 创建表
fmt.Println("\n=== 示例 2: 使用 Schema 创建表 ===")
table, err := srdb.OpenTable(&srdb.TableOptions{
Dir: "./data/users",
Name: schema.Name,
Fields: schema.Fields,
})
if err != nil {
log.Fatal(err)
}
defer table.Close()
// 插入数据
err = table.Insert(map[string]any{
"name": "张三",
"age": int64(25),
"email": "zhangsan@example.com",
"score": 95.5,
"is_active": true,
})
if err != nil {
log.Fatal(err)
}
err = table.Insert(map[string]any{
"name": "李四",
"age": int64(30),
"email": "lisi@example.com",
"score": 88.0,
"is_active": true,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("✓ 插入 2 条数据")
// 查询数据
rows, err := table.Query().Eq("email", "zhangsan@example.com").Rows()
if err != nil {
log.Fatal(err)
}
defer rows.Close()
fmt.Println("\n查询结果 (email = zhangsan@example.com):")
for rows.Next() {
data := rows.Row().Data()
fmt.Printf(" 姓名: %s, 年龄: %v, 邮箱: %s, 分数: %v, 激活: %v\n",
data["name"], data["age"], data["email"], data["score"], data["is_active"])
}
// 示例 3: 使用默认字段名snake_case
fmt.Println("\n=== 示例 3: 使用默认字段名snake_case===")
productFields, err := srdb.StructToFields(Product{})
if err != nil {
log.Fatal(err)
}
fmt.Println("Product 字段(使用默认 snake_case 名称):")
for _, field := range productFields {
fmt.Printf(" - %s (%s)\n", field.Name, field.Type.String())
}
// 示例 4: 获取索引字段
fmt.Println("\n=== 示例 4: 获取索引字段 ===")
indexedFields := schema.GetIndexedFields()
fmt.Printf("User Schema 中的索引字段(共 %d 个):\n", len(indexedFields))
for _, field := range indexedFields {
fmt.Printf(" - %s: %s\n", field.Name, field.Comment)
}
fmt.Println("\n✓ 所有示例执行成功!")
}

View File

@@ -1,132 +0,0 @@
# 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
✓ 插入用户 bobemail、bio、balance 为 NULL
5. 测试必填字段不能为 NULL
✓ 符合预期的错误: field username: NULL value not allowed (field is not nullable)
6. 查询所有用户
用户: alice, 邮箱: alice@example.com, 余额: 1000.5
用户: bob, 邮箱: <NULL>, 余额: <NULL>
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
```

View File

@@ -1,10 +0,0 @@
{
"version": 1,
"tables": [
{
"name": "users",
"dir": "users",
"created_at": 1760032030
}
]
}

View File

@@ -1 +0,0 @@
MANIFEST-000001

View File

@@ -1,66 +0,0 @@
{
"version": 1,
"timestamp": 1760032030,
"checksum": "1fdebae19ecf3cfcecddfa11ddcfc9c4b6656e1fd2477df8849639c108f4ada1",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "username",
"Type": 13,
"Indexed": true,
"Nullable": false,
"Comment": "用户名(索引)"
},
{
"Name": "age",
"Type": 5,
"Indexed": false,
"Nullable": false,
"Comment": "年龄"
},
{
"Name": "email",
"Type": 13,
"Indexed": false,
"Nullable": true,
"Comment": "邮箱(可选)"
},
{
"Name": "phone_number",
"Type": 13,
"Indexed": true,
"Nullable": true,
"Comment": "手机号(可空且索引)"
},
{
"Name": "bio",
"Type": 13,
"Indexed": false,
"Nullable": true,
"Comment": "个人简介(可选)"
},
{
"Name": "avatar",
"Type": 13,
"Indexed": false,
"Nullable": true,
"Comment": "头像 URL可选"
},
{
"Name": "balance",
"Type": 17,
"Indexed": false,
"Nullable": true,
"Comment": "账户余额(可空)"
},
{
"Name": "is_active",
"Type": 14,
"Indexed": false,
"Nullable": false,
"Comment": "是否激活"
}
]
}
}

View File

@@ -1 +0,0 @@
2

View File

@@ -1,15 +0,0 @@
module code.tczkiot.com/wlw/srdb/examples/struct_tags
go 1.24.0
replace code.tczkiot.com/wlw/srdb => ../..
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
)

View File

@@ -1,6 +0,0 @@
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=

View File

@@ -1,176 +0,0 @@
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("✓ 插入用户 bobemail、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(", 邮箱: <NULL>")
} else {
fmt.Printf(", 邮箱: %v", email)
}
if balance == nil {
fmt.Printf(", 余额: <NULL>")
} 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:\"-\" # 忽略字段")
}

View File

@@ -1,255 +0,0 @@
# 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
- 内存开销无额外分配
- 向后兼容零性能损失

View File

@@ -1,201 +0,0 @@
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(" • 完全向后兼容旧格式")
}

View File

@@ -1,140 +0,0 @@
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)
}

View File

@@ -1,106 +0,0 @@
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)
}

View File

@@ -0,0 +1,20 @@
{
"version": 1,
"tables": [
{
"name": "users",
"dir": "users",
"created_at": 1760073183
},
{
"name": "products",
"dir": "products",
"created_at": 1760073183
},
{
"name": "logs",
"dir": "logs",
"created_at": 1760073184
}
]
}

Binary file not shown.

View File

@@ -0,0 +1,38 @@
{
"version": 1,
"timestamp": 1760073184,
"checksum": "3ebde7026b498f38e4dd85058c995fff4a1a51bdb5a1254ead9a7e89030b1b76",
"schema": {
"Name": "logs",
"Fields": [
{
"Name": "group",
"Type": 13,
"Indexed": true,
"Nullable": false,
"Comment": "Log group (A-E)"
},
{
"Name": "timestamp",
"Type": 13,
"Indexed": false,
"Nullable": false,
"Comment": "Timestamp"
},
{
"Name": "data",
"Type": 13,
"Indexed": false,
"Nullable": false,
"Comment": "Random data"
},
{
"Name": "size_bytes",
"Type": 5,
"Indexed": false,
"Nullable": false,
"Comment": "Data size in bytes"
}
]
}
}

View File

@@ -0,0 +1 @@
151

View File

@@ -0,0 +1,38 @@
{
"version": 1,
"timestamp": 1760073183,
"checksum": "6bfaccc47c2ff24f377b6536a800591958a3680be4f99e8f78c1d8e22e78afd4",
"schema": {
"Name": "products",
"Fields": [
{
"Name": "product_name",
"Type": 13,
"Indexed": true,
"Nullable": false,
"Comment": "Product name"
},
{
"Name": "price",
"Type": 12,
"Indexed": false,
"Nullable": false,
"Comment": "Price"
},
{
"Name": "quantity",
"Type": 5,
"Indexed": false,
"Nullable": false,
"Comment": "Quantity"
},
{
"Name": "category",
"Type": 13,
"Indexed": false,
"Nullable": false,
"Comment": "Category"
}
]
}
}

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,38 @@
{
"version": 1,
"timestamp": 1760073183,
"checksum": "da17386c778ecfe06d2560097e7e6daa241f59de569517db3cca7424aec345f4",
"schema": {
"Name": "users",
"Fields": [
{
"Name": "name",
"Type": 13,
"Indexed": true,
"Nullable": false,
"Comment": "User name"
},
{
"Name": "email",
"Type": 13,
"Indexed": false,
"Nullable": false,
"Comment": "Email address"
},
{
"Name": "age",
"Type": 5,
"Indexed": false,
"Nullable": false,
"Comment": "Age"
},
{
"Name": "city",
"Type": 13,
"Indexed": false,
"Nullable": false,
"Comment": "City"
}
]
}
}

View File

@@ -0,0 +1 @@
1

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps" "maps"
"reflect"
"strings" "strings"
) )
@@ -658,7 +659,8 @@ func (r *Row) Scan(value any) error {
return fmt.Errorf("row is nil") return fmt.Errorf("row is nil")
} }
data, err := json.Marshal(r.inner.Data) // 使用 r.Data() 而不是 r.inner.Data这样会应用字段过滤
data, err := json.Marshal(r.Data())
if err != nil { if err != nil {
return fmt.Errorf("marshal row data: %w", err) return fmt.Errorf("marshal row data: %w", err)
} }
@@ -920,18 +922,40 @@ func (r *Rows) Data() []map[string]any {
} }
// Scan 扫描所有行数据到指定的变量 // Scan 扫描所有行数据到指定的变量
// 智能判断目标类型:
// - 如果目标是切片:扫描所有行
// - 如果目标是结构体/指针:只扫描第一行
func (r *Rows) Scan(value any) error { func (r *Rows) Scan(value any) error {
data, err := json.Marshal(r.Collect()) rv := reflect.ValueOf(value)
if err != nil { if rv.Kind() != reflect.Ptr {
return fmt.Errorf("marshal rows data: %w", err) return fmt.Errorf("scan target must be a pointer")
} }
err = json.Unmarshal(data, value) elem := rv.Elem()
if err != nil { kind := elem.Kind()
return fmt.Errorf("unmarshal to target: %w", err)
// 如果目标是切片,扫描所有行
if kind == reflect.Slice {
data, err := json.Marshal(r.Collect())
if err != nil {
return fmt.Errorf("marshal rows data: %w", err)
}
err = json.Unmarshal(data, value)
if err != nil {
return fmt.Errorf("unmarshal to target: %w", err)
}
return nil
} }
return nil // 否则,只扫描第一行
row, err := r.First()
if err != nil {
return err
}
return row.Scan(value)
} }
// First 获取第一行 // First 获取第一行

View File

@@ -52,6 +52,10 @@ const (
// 时间类型 // 时间类型
Time // time.Time 时间戳 Time // time.Time 时间戳
Duration // time.Duration 时间间隔 Duration // time.Duration 时间间隔
// 复杂类型
Object // map[string]xxx、struct{}、*struct{}
Array // 切片类型 []xxx
) )
func (t FieldType) String() string { func (t FieldType) String() string {
@@ -94,6 +98,10 @@ func (t FieldType) String() string {
return "time" return "time"
case Duration: case Duration:
return "duration" return "duration"
case Object:
return "object"
case Array:
return "array"
default: default:
return "unknown" return "unknown"
} }
@@ -370,6 +378,18 @@ func goTypeToFieldType(typ reflect.Type) (FieldType, error) {
return String, nil return String, nil
case reflect.Bool: case reflect.Bool:
return Bool, nil return Bool, nil
case reflect.Map:
// map[string]xxx → Object
if typ.Key().Kind() != reflect.String {
return 0, fmt.Errorf("map key must be string, got %s", typ.Key().Kind())
}
return Object, nil
case reflect.Struct:
// struct{} → Object (排除特殊类型 time.Time、decimal.Decimal 等已在前面处理)
return Object, nil
case reflect.Slice:
// []xxx → Array
return Array, nil
default: default:
return 0, fmt.Errorf("unsupported type: %s", typ.Kind()) return 0, fmt.Errorf("unsupported type: %s", typ.Kind())
} }
@@ -665,6 +685,32 @@ func (s *Schema) validateType(typ FieldType, value any) error {
return fmt.Errorf("expected duration type, got %T", value) return fmt.Errorf("expected duration type, got %T", value)
} }
// Object 类型
case Object:
v := reflect.ValueOf(value)
kind := v.Kind()
if kind == reflect.Map {
// map[string]xxx
if v.Type().Key().Kind() != reflect.String {
return fmt.Errorf("expected map[string]xxx, got %T", value)
}
return nil
} else if kind == reflect.Struct {
// struct{}
return nil
} else if kind == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
// *struct{}
return nil
}
return fmt.Errorf("expected object type (map[string]xxx, struct{} or *struct{}), got %T", value)
// Array 类型
case Array:
if reflect.ValueOf(value).Kind() != reflect.Slice {
return fmt.Errorf("expected slice type, got %T", value)
}
return nil
default: default:
return fmt.Errorf("unknown field type: %v", typ) return fmt.Errorf("unknown field type: %v", typ)
} }
@@ -751,6 +797,24 @@ func convertValue(value any, targetType FieldType) (any, error) {
case Duration: case Duration:
return convertToDuration(value) return convertToDuration(value)
// Object 类型
case Object:
// Object 类型不需要转换,直接返回
v := reflect.ValueOf(value)
kind := v.Kind()
if kind == reflect.Map || kind == reflect.Struct || (kind == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
return value, nil
}
return nil, fmt.Errorf("cannot convert %T to object", value)
// Array 类型
case Array:
// Array 类型不需要转换,直接返回
if reflect.ValueOf(value).Kind() == reflect.Slice {
return value, nil
}
return nil, fmt.Errorf("cannot convert %T to array", value)
default: default:
return nil, fmt.Errorf("unsupported type: %v", targetType) return nil, fmt.Errorf("unsupported type: %v", targetType)
} }

View File

@@ -3,6 +3,7 @@ package srdb
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -384,6 +385,36 @@ func writeFieldBinaryValue(buf *bytes.Buffer, typ FieldType, value any) error {
// 存储为纳秒int64 // 存储为纳秒int64
return binary.Write(buf, binary.LittleEndian, int64(v)) return binary.Write(buf, binary.LittleEndian, int64(v))
// Object 类型(使用 JSON 编码)
case Object:
// 使用 JSON 序列化
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("marshal object: %w", err)
}
// 写入长度
if err := binary.Write(buf, binary.LittleEndian, uint32(len(data))); err != nil {
return err
}
// 写入数据
_, err = buf.Write(data)
return err
// Array 类型(使用 JSON 编码)
case Array:
// 使用 JSON 序列化
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("marshal array: %w", err)
}
// 写入长度
if err := binary.Write(buf, binary.LittleEndian, uint32(len(data))); err != nil {
return err
}
// 写入数据
_, err = buf.Write(data)
return err
default: default:
return fmt.Errorf("unsupported field type: %d", typ) return fmt.Errorf("unsupported field type: %d", typ)
} }
@@ -451,6 +482,24 @@ func writeFieldZeroValue(buf *bytes.Buffer, typ FieldType) error {
case Duration: case Duration:
return binary.Write(buf, binary.LittleEndian, int64(0)) return binary.Write(buf, binary.LittleEndian, int64(0))
// Object 类型(零值:空 JSON 对象 {}
case Object:
data := []byte("{}")
if err := binary.Write(buf, binary.LittleEndian, uint32(len(data))); err != nil {
return err
}
_, err := buf.Write(data)
return err
// Array 类型(零值:空 JSON 数组 []
case Array:
data := []byte("[]")
if err := binary.Write(buf, binary.LittleEndian, uint32(len(data))); err != nil {
return err
}
_, err := buf.Write(data)
return err
default: default:
return fmt.Errorf("unsupported field type: %d", typ) return fmt.Errorf("unsupported field type: %d", typ)
} }
@@ -784,6 +833,44 @@ func readFieldBinaryValue(buf *bytes.Reader, typ FieldType, keep bool) (any, err
} }
return nil, nil return nil, nil
// Object 类型(使用 JSON 解码)
case Object:
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 obj map[string]any
if err := json.Unmarshal(data, &obj); err != nil {
return nil, fmt.Errorf("unmarshal object: %w", err)
}
return obj, nil
}
return nil, nil
// Array 类型(使用 JSON 解码)
case Array:
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 arr []any
if err := json.Unmarshal(data, &arr); err != nil {
return nil, fmt.Errorf("unmarshal array: %w", err)
}
return arr, nil
}
return nil, nil
default: default:
return nil, fmt.Errorf("unsupported field type: %d", typ) return nil, fmt.Errorf("unsupported field type: %d", typ)
} }