重构:清理项目结构和完善文档
- 添加完整的 DOCS.md 文档(1376 行) - 更新 README.md,增强项目说明 - 清理临时示例和测试数据 - 删除诊断工具(已完成测试) - 为 webui 示例准备测试数据 - 优化 .gitignore 配置 - 增强 Query 和 Schema 功能 - 改进 SSTable 编码处理
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -36,11 +36,12 @@ testdb/
|
||||
*.sst
|
||||
|
||||
# Example binaries
|
||||
/examples/webui/data/
|
||||
/examples/*/data/
|
||||
|
||||
# AI markdown
|
||||
/*.md
|
||||
!/CLAUDE.md
|
||||
!/DESIGN.md
|
||||
!/README.md
|
||||
!/DOCS.md
|
||||
!/LICENSE.md
|
||||
!/README.md
|
||||
|
||||
@@ -142,13 +142,14 @@ table, _ := db.CreateTable("users", schema)
|
||||
- Schema 在 `Insert()` 时强制验证类型和必填字段
|
||||
- 索引字段(`Indexed: true`)自动创建二级索引
|
||||
- Schema 持久化到 `table_dir/schema.json`,包含校验和防篡改
|
||||
- **支持的类型** (17 种,精确映射到 Go 基础类型):
|
||||
- **支持的类型** (21 种,精确映射到 Go 基础类型):
|
||||
- **有符号整数** (5种): `Int`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||
- **无符号整数** (5种): `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
|
||||
- **浮点数** (2种): `Float32`, `Float64`
|
||||
- **字符串** (1种): `String`
|
||||
- **布尔** (1种): `Bool`
|
||||
- **特殊类型** (3种): `Byte` (独立类型,底层=uint8), `Rune` (独立类型,底层=int32), `Decimal` (高精度十进制,使用 shopspring/decimal)
|
||||
- **特殊类型** (5种): `Byte` (独立类型,底层=uint8), `Rune` (独立类型,底层=int32), `Decimal` (高精度十进制,使用 shopspring/decimal), `Time` (time.Time), `Duration` (time.Duration)
|
||||
- **复杂类型** (2种): `Object` (map[string]xxx、struct{}、*struct{},使用 JSON 编码), `Array` ([]xxx 切片,使用 JSON 编码)
|
||||
- **Nullable 支持**: 字段可标记为 `Nullable: true`,允许 NULL 值
|
||||
|
||||
### 类型系统详解
|
||||
|
||||
185
README.md
185
README.md
@@ -13,7 +13,8 @@
|
||||
- **WAL 持久化** - 写前日志保证数据安全
|
||||
- **自动 Compaction** - 智能的多层级数据合并策略
|
||||
- **索引支持** - 快速的字段查询能力
|
||||
- **Schema 管理** - 灵活的表结构定义
|
||||
- **Schema 管理** - 灵活的表结构定义,支持 21 种类型
|
||||
- **复杂类型** - 原生支持 Object(map)和 Array(slice)
|
||||
|
||||
### 查询能力
|
||||
- **链式查询 API** - 流畅的查询构建器
|
||||
@@ -21,6 +22,7 @@
|
||||
- **复合条件** - `AND`, `OR`, `NOT` 逻辑组合
|
||||
- **字段选择** - 按需加载指定字段,优化性能
|
||||
- **游标模式** - 惰性加载,支持大数据集遍历
|
||||
- **智能 Scan** - 自动扫描到结构体,完整支持复杂类型
|
||||
|
||||
### 管理工具
|
||||
- **Web UI** - 现代化的数据库管理界面
|
||||
@@ -34,10 +36,13 @@
|
||||
- [快速开始](#快速开始)
|
||||
- [基本用法](#基本用法)
|
||||
- [查询 API](#查询-api)
|
||||
- [Scan 方法](#scan-方法---扫描到结构体)
|
||||
- [Object 和 Array 类型](#object-和-array-类型)
|
||||
- [Web UI](#web-ui)
|
||||
- [架构设计](#架构设计)
|
||||
- [性能特点](#性能特点)
|
||||
- [开发指南](#开发指南)
|
||||
- [文档](#文档)
|
||||
|
||||
---
|
||||
|
||||
@@ -69,12 +74,15 @@ func main() {
|
||||
defer db.Close()
|
||||
|
||||
// 2. 定义 Schema
|
||||
schema := srdb.NewSchema("users", []srdb.Field{
|
||||
{Name: "id", Type: srdb.FieldTypeInt64, Indexed: true, Comment: "用户ID"},
|
||||
{Name: "name", Type: srdb.FieldTypeString, Indexed: false, Comment: "用户名"},
|
||||
{Name: "email", Type: srdb.FieldTypeString, Indexed: true, Comment: "邮箱"},
|
||||
{Name: "age", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "年龄"},
|
||||
schema, err := srdb.NewSchema("users", []srdb.Field{
|
||||
{Name: "id", Type: srdb.Int64, Indexed: true, Comment: "用户ID"},
|
||||
{Name: "name", Type: srdb.String, Indexed: false, Comment: "用户名"},
|
||||
{Name: "email", Type: srdb.String, Indexed: true, Comment: "邮箱"},
|
||||
{Name: "age", Type: srdb.Int32, Indexed: false, Comment: "年龄"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 3. 创建表
|
||||
table, err := db.CreateTable("users", schema)
|
||||
@@ -158,33 +166,64 @@ err := table.Update(seq, map[string]any{
|
||||
### Schema 定义
|
||||
|
||||
```go
|
||||
schema := srdb.NewSchema("logs", []srdb.Field{
|
||||
schema, err := srdb.NewSchema("logs", []srdb.Field{
|
||||
{
|
||||
Name: "group",
|
||||
Type: srdb.FieldTypeString,
|
||||
Name: "level",
|
||||
Type: srdb.String,
|
||||
Indexed: true,
|
||||
Comment: "日志分组",
|
||||
Comment: "日志级别",
|
||||
},
|
||||
{
|
||||
Name: "message",
|
||||
Type: srdb.FieldTypeString,
|
||||
Type: srdb.String,
|
||||
Indexed: false,
|
||||
Comment: "日志内容",
|
||||
},
|
||||
{
|
||||
Name: "timestamp",
|
||||
Type: srdb.FieldTypeInt64,
|
||||
Type: srdb.Int64,
|
||||
Indexed: true,
|
||||
Comment: "时间戳",
|
||||
},
|
||||
{
|
||||
Name: "metadata",
|
||||
Type: srdb.Object,
|
||||
Indexed: false,
|
||||
Comment: "元数据(map)",
|
||||
},
|
||||
{
|
||||
Name: "tags",
|
||||
Type: srdb.Array,
|
||||
Indexed: false,
|
||||
Comment: "标签(slice)",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**支持的字段类型**:
|
||||
- `FieldTypeString` - 字符串
|
||||
- `FieldTypeInt64` - 64位整数
|
||||
- `FieldTypeBool` - 布尔值
|
||||
- `FieldTypeFloat64` - 64位浮点数
|
||||
**支持的字段类型**(21 种):
|
||||
|
||||
**有符号整数**:
|
||||
- `Int`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||
|
||||
**无符号整数**:
|
||||
- `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()
|
||||
|
||||
// 扫描到结构体
|
||||
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 结构体)
|
||||
- ✅ 完整支持 Object(map)和 Array(slice)类型
|
||||
- ✅ 支持嵌套结构
|
||||
- ✅ 结合 Select() 优化性能
|
||||
|
||||
详细示例:[examples/scan_demo](examples/scan_demo/README.md)
|
||||
|
||||
### 完整的操作符列表
|
||||
|
||||
| 操作符 | 方法 | 说明 |
|
||||
@@ -317,6 +395,61 @@ err := rows.Scan(&users)
|
||||
| `IS NULL` | `IsNull(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
|
||||
@@ -497,10 +630,18 @@ go build -o webui main.go
|
||||
|
||||
## 📚 文档
|
||||
|
||||
### 核心文档
|
||||
- [设计文档](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 参考
|
||||
|
||||
### 示例和教程
|
||||
- [Scan 方法指南](examples/scan_demo/README.md) - 扫描到结构体,支持 Object 和 Array
|
||||
- [WebUI 工具](examples/webui/README.md) - Web 管理界面使用指南
|
||||
- [所有类型示例](examples/all_types/) - 21 种类型的完整示例
|
||||
- [Nullable 示例](examples/nullable/) - Nullable 字段的使用
|
||||
|
||||
---
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
@@ -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": "是否在线"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -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 (语义清晰)")
|
||||
}
|
||||
@@ -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 示例
|
||||
Binary file not shown.
@@ -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": "年龄"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
Binary file not shown.
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST-000001
|
||||
Binary file not shown.
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST-000001
|
||||
Binary file not shown.
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST-000001
|
||||
Binary file not shown.
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST-000001
|
||||
Binary file not shown.
@@ -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": "是否有货"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -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"])
|
||||
}
|
||||
@@ -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 类型保证金融数据精确性
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST-000001
|
||||
@@ -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": "响应时间(毫秒)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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 习惯")
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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 等复杂类型
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST-000001
|
||||
Binary file not shown.
@@ -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": "是否激活"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
3
|
||||
@@ -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✓ 所有示例执行成功!")
|
||||
}
|
||||
@@ -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)
|
||||
✓ 插入用户 bob(email、bio、balance 为 NULL)
|
||||
|
||||
5. 测试必填字段不能为 NULL
|
||||
✓ 符合预期的错误: field username: NULL value not allowed (field is not nullable)
|
||||
|
||||
6. 查询所有用户
|
||||
用户: alice, 邮箱: alice@example.com, 余额: 1000.5
|
||||
用户: bob, 邮箱: <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
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"tables": [
|
||||
{
|
||||
"name": "users",
|
||||
"dir": "users",
|
||||
"created_at": 1760032030
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
MANIFEST-000001
|
||||
Binary file not shown.
@@ -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": "是否激活"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
2
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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("✓ 插入用户 bob(email、bio、balance 为 NULL)")
|
||||
|
||||
// 5. 插入数据 - 必填字段不能为 NULL
|
||||
fmt.Println("\n5. 测试必填字段不能为 NULL")
|
||||
err = table.Insert(map[string]any{
|
||||
"username": nil, // 尝试将必填字段设为 NULL
|
||||
"age": int64(28),
|
||||
"is_active": true,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("✓ 符合预期的错误: %v\n", err)
|
||||
} else {
|
||||
log.Fatal("应该返回错误,但成功了!")
|
||||
}
|
||||
|
||||
// 6. 查询所有数据
|
||||
fmt.Println("\n6. 查询所有用户")
|
||||
rows, err := table.Query().Rows()
|
||||
if err != nil {
|
||||
log.Fatalf("Query failed: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
row := rows.Row()
|
||||
data := row.Data()
|
||||
|
||||
username := data["username"]
|
||||
email := data["email"]
|
||||
balance := data["balance"]
|
||||
|
||||
fmt.Printf(" 用户: %v", username)
|
||||
if email == nil {
|
||||
fmt.Printf(", 邮箱: <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:\"-\" # 忽略字段")
|
||||
}
|
||||
@@ -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
|
||||
- 内存开销:无额外分配
|
||||
- 向后兼容:零性能损失
|
||||
@@ -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(" • 完全向后兼容旧格式")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
20
examples/webui/data/database.meta
Normal file
20
examples/webui/data/database.meta
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
examples/webui/data/logs/MANIFEST-000001
Normal file
BIN
examples/webui/data/logs/MANIFEST-000001
Normal file
Binary file not shown.
38
examples/webui/data/logs/schema.json
Normal file
38
examples/webui/data/logs/schema.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1
examples/webui/data/logs/wal/CURRENT
Normal file
1
examples/webui/data/logs/wal/CURRENT
Normal file
@@ -0,0 +1 @@
|
||||
151
|
||||
38
examples/webui/data/products/schema.json
Normal file
38
examples/webui/data/products/schema.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1
examples/webui/data/products/wal/CURRENT
Normal file
1
examples/webui/data/products/wal/CURRENT
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
38
examples/webui/data/users/schema.json
Normal file
38
examples/webui/data/users/schema.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1
examples/webui/data/users/wal/CURRENT
Normal file
1
examples/webui/data/users/wal/CURRENT
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
40
query.go
40
query.go
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -658,7 +659,8 @@ func (r *Row) Scan(value any) error {
|
||||
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 {
|
||||
return fmt.Errorf("marshal row data: %w", err)
|
||||
}
|
||||
@@ -920,18 +922,40 @@ func (r *Rows) Data() []map[string]any {
|
||||
}
|
||||
|
||||
// Scan 扫描所有行数据到指定的变量
|
||||
// 智能判断目标类型:
|
||||
// - 如果目标是切片:扫描所有行
|
||||
// - 如果目标是结构体/指针:只扫描第一行
|
||||
func (r *Rows) Scan(value any) error {
|
||||
data, err := json.Marshal(r.Collect())
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal rows data: %w", err)
|
||||
rv := reflect.ValueOf(value)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("scan target must be a pointer")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal to target: %w", err)
|
||||
elem := rv.Elem()
|
||||
kind := elem.Kind()
|
||||
|
||||
// 如果目标是切片,扫描所有行
|
||||
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 获取第一行
|
||||
|
||||
64
schema.go
64
schema.go
@@ -52,6 +52,10 @@ const (
|
||||
// 时间类型
|
||||
Time // time.Time 时间戳
|
||||
Duration // time.Duration 时间间隔
|
||||
|
||||
// 复杂类型
|
||||
Object // map[string]xxx、struct{}、*struct{}
|
||||
Array // 切片类型 []xxx
|
||||
)
|
||||
|
||||
func (t FieldType) String() string {
|
||||
@@ -94,6 +98,10 @@ func (t FieldType) String() string {
|
||||
return "time"
|
||||
case Duration:
|
||||
return "duration"
|
||||
case Object:
|
||||
return "object"
|
||||
case Array:
|
||||
return "array"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
@@ -370,6 +378,18 @@ func goTypeToFieldType(typ reflect.Type) (FieldType, error) {
|
||||
return String, nil
|
||||
case reflect.Bool:
|
||||
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:
|
||||
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)
|
||||
}
|
||||
|
||||
// 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:
|
||||
return fmt.Errorf("unknown field type: %v", typ)
|
||||
}
|
||||
@@ -751,6 +797,24 @@ func convertValue(value any, targetType FieldType) (any, error) {
|
||||
case Duration:
|
||||
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:
|
||||
return nil, fmt.Errorf("unsupported type: %v", targetType)
|
||||
}
|
||||
|
||||
87
sstable.go
87
sstable.go
@@ -3,6 +3,7 @@ package srdb
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -384,6 +385,36 @@ func writeFieldBinaryValue(buf *bytes.Buffer, typ FieldType, value any) error {
|
||||
// 存储为纳秒(int64)
|
||||
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:
|
||||
return fmt.Errorf("unsupported field type: %d", typ)
|
||||
}
|
||||
@@ -451,6 +482,24 @@ func writeFieldZeroValue(buf *bytes.Buffer, typ FieldType) error {
|
||||
case Duration:
|
||||
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:
|
||||
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
|
||||
|
||||
// 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:
|
||||
return nil, fmt.Errorf("unsupported field type: %d", typ)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user