From 7d2bb4745c8666fb2bcb7fcc3529eda8dc98e264 Mon Sep 17 00:00:00 2001 From: bourdon Date: Fri, 10 Oct 2025 16:38:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E6=B8=85=E7=90=86?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84=E5=92=8C=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加完整的 DOCS.md 文档(1376 行) - 更新 README.md,增强项目说明 - 清理临时示例和测试数据 - 删除诊断工具(已完成测试) - 为 webui 示例准备测试数据 - 优化 .gitignore 配置 - 增强 Query 和 Schema 功能 - 改进 SSTable 编码处理 --- .gitignore | 5 +- CLAUDE.md | 5 +- DOCS.md | 1376 +++++++++++++++++ README.md | 185 ++- examples/all_types/data/sensors/schema.json | 34 - examples/all_types/data/sensors/wal/CURRENT | 1 - examples/all_types/main.go | 98 -- examples/batch_insert/README.md | 162 -- .../data/example1/MANIFEST-000001 | Bin 242 -> 0 bytes .../batch_insert/data/example1/schema.json | 22 - .../batch_insert/data/example1/wal/CURRENT | 1 - .../data/example2/MANIFEST-000001 | Bin 242 -> 0 bytes .../batch_insert/data/example2/schema.json | 28 - .../batch_insert/data/example2/wal/CURRENT | 1 - examples/batch_insert/data/example3/CURRENT | 1 - .../data/example3/MANIFEST-000001 | Bin 242 -> 0 bytes .../batch_insert/data/example3/schema.json | 34 - .../batch_insert/data/example3/wal/CURRENT | 1 - examples/batch_insert/data/example4/CURRENT | 1 - .../data/example4/MANIFEST-000001 | Bin 242 -> 0 bytes .../batch_insert/data/example4/schema.json | 34 - .../batch_insert/data/example4/wal/CURRENT | 1 - examples/batch_insert/data/example5/CURRENT | 1 - .../data/example5/MANIFEST-000001 | Bin 242 -> 0 bytes .../batch_insert/data/example5/schema.json | 34 - .../batch_insert/data/example5/wal/CURRENT | 1 - examples/batch_insert/data/example6/CURRENT | 1 - .../data/example6/MANIFEST-000001 | Bin 242 -> 0 bytes .../batch_insert/data/example6/schema.json | 34 - .../batch_insert/data/example6/wal/CURRENT | 1 - examples/batch_insert/main.go | 321 ---- examples/new_types/README.md | 129 -- examples/new_types/data/api_logs/CURRENT | 1 - examples/new_types/data/api_logs/schema.json | 31 - examples/new_types/data/api_logs/wal/CURRENT | 1 - examples/new_types/go.mod | 15 - examples/new_types/go.sum | 6 - examples/new_types/main.go | 378 ----- examples/nullable/README.md | 353 ----- examples/nullable/main.go | 289 ---- examples/snake_case_demo/main.go | 101 -- examples/struct_schema/README.md | 153 -- examples/struct_schema/data/users/CURRENT | 1 - .../struct_schema/data/users/MANIFEST-000001 | Bin 405 -> 0 bytes examples/struct_schema/data/users/schema.json | 40 - examples/struct_schema/data/users/wal/CURRENT | 1 - examples/struct_schema/main.go | 133 -- examples/struct_tags/README.md | 132 -- examples/struct_tags/data/database.meta | 10 - examples/struct_tags/data/users/CURRENT | 1 - .../struct_tags/data/users/MANIFEST-000001 | Bin 79 -> 0 bytes examples/struct_tags/data/users/schema.json | 66 - examples/struct_tags/data/users/wal/CURRENT | 1 - examples/struct_tags/go.mod | 15 - examples/struct_tags/go.sum | 6 - examples/struct_tags/main.go | 176 --- examples/tag_format/README.md | 255 --- examples/tag_format/main.go | 201 --- examples/time_duration/main.go | 140 -- examples/time_duration_simple/main.go | 106 -- examples/webui/data/database.meta | 20 + .../data/sensors => webui/data/logs}/CURRENT | 0 examples/webui/data/logs/MANIFEST-000001 | Bin 0 -> 63545 bytes examples/webui/data/logs/schema.json | 38 + examples/webui/data/logs/wal/CURRENT | 1 + .../example1 => webui/data/products}/CURRENT | 0 .../data/products}/MANIFEST-000001 | Bin examples/webui/data/products/schema.json | 38 + examples/webui/data/products/wal/CURRENT | 1 + .../example2 => webui/data/users}/CURRENT | 0 .../data/users}/MANIFEST-000001 | Bin examples/webui/data/users/schema.json | 38 + examples/webui/data/users/wal/CURRENT | 1 + query.go | 40 +- schema.go | 64 + sstable.go | 87 ++ 76 files changed, 1865 insertions(+), 3587 deletions(-) create mode 100644 DOCS.md delete mode 100644 examples/all_types/data/sensors/schema.json delete mode 100644 examples/all_types/data/sensors/wal/CURRENT delete mode 100644 examples/all_types/main.go delete mode 100644 examples/batch_insert/README.md delete mode 100644 examples/batch_insert/data/example1/MANIFEST-000001 delete mode 100644 examples/batch_insert/data/example1/schema.json delete mode 100644 examples/batch_insert/data/example1/wal/CURRENT delete mode 100644 examples/batch_insert/data/example2/MANIFEST-000001 delete mode 100644 examples/batch_insert/data/example2/schema.json delete mode 100644 examples/batch_insert/data/example2/wal/CURRENT delete mode 100644 examples/batch_insert/data/example3/CURRENT delete mode 100644 examples/batch_insert/data/example3/MANIFEST-000001 delete mode 100644 examples/batch_insert/data/example3/schema.json delete mode 100644 examples/batch_insert/data/example3/wal/CURRENT delete mode 100644 examples/batch_insert/data/example4/CURRENT delete mode 100644 examples/batch_insert/data/example4/MANIFEST-000001 delete mode 100644 examples/batch_insert/data/example4/schema.json delete mode 100644 examples/batch_insert/data/example4/wal/CURRENT delete mode 100644 examples/batch_insert/data/example5/CURRENT delete mode 100644 examples/batch_insert/data/example5/MANIFEST-000001 delete mode 100644 examples/batch_insert/data/example5/schema.json delete mode 100644 examples/batch_insert/data/example5/wal/CURRENT delete mode 100644 examples/batch_insert/data/example6/CURRENT delete mode 100644 examples/batch_insert/data/example6/MANIFEST-000001 delete mode 100644 examples/batch_insert/data/example6/schema.json delete mode 100644 examples/batch_insert/data/example6/wal/CURRENT delete mode 100644 examples/batch_insert/main.go delete mode 100644 examples/new_types/README.md delete mode 100644 examples/new_types/data/api_logs/CURRENT delete mode 100644 examples/new_types/data/api_logs/schema.json delete mode 100644 examples/new_types/data/api_logs/wal/CURRENT delete mode 100644 examples/new_types/go.mod delete mode 100644 examples/new_types/go.sum delete mode 100644 examples/new_types/main.go delete mode 100644 examples/nullable/README.md delete mode 100644 examples/nullable/main.go delete mode 100644 examples/snake_case_demo/main.go delete mode 100644 examples/struct_schema/README.md delete mode 100644 examples/struct_schema/data/users/CURRENT delete mode 100644 examples/struct_schema/data/users/MANIFEST-000001 delete mode 100644 examples/struct_schema/data/users/schema.json delete mode 100644 examples/struct_schema/data/users/wal/CURRENT delete mode 100644 examples/struct_schema/main.go delete mode 100644 examples/struct_tags/README.md delete mode 100644 examples/struct_tags/data/database.meta delete mode 100644 examples/struct_tags/data/users/CURRENT delete mode 100644 examples/struct_tags/data/users/MANIFEST-000001 delete mode 100644 examples/struct_tags/data/users/schema.json delete mode 100644 examples/struct_tags/data/users/wal/CURRENT delete mode 100644 examples/struct_tags/go.mod delete mode 100644 examples/struct_tags/go.sum delete mode 100644 examples/struct_tags/main.go delete mode 100644 examples/tag_format/README.md delete mode 100644 examples/tag_format/main.go delete mode 100644 examples/time_duration/main.go delete mode 100644 examples/time_duration_simple/main.go create mode 100644 examples/webui/data/database.meta rename examples/{all_types/data/sensors => webui/data/logs}/CURRENT (100%) create mode 100644 examples/webui/data/logs/MANIFEST-000001 create mode 100644 examples/webui/data/logs/schema.json create mode 100644 examples/webui/data/logs/wal/CURRENT rename examples/{batch_insert/data/example1 => webui/data/products}/CURRENT (100%) rename examples/{all_types/data/sensors => webui/data/products}/MANIFEST-000001 (100%) create mode 100644 examples/webui/data/products/schema.json create mode 100644 examples/webui/data/products/wal/CURRENT rename examples/{batch_insert/data/example2 => webui/data/users}/CURRENT (100%) rename examples/{new_types/data/api_logs => webui/data/users}/MANIFEST-000001 (100%) create mode 100644 examples/webui/data/users/schema.json create mode 100644 examples/webui/data/users/wal/CURRENT diff --git a/.gitignore b/.gitignore index 9637acc..e54ecfa 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index bb14dbb..1a8f3ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.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 值 ### 类型系统详解 diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..7dd3cbc --- /dev/null +++ b/DOCS.md @@ -0,0 +1,1376 @@ +# SRDB 完整文档 + +## 目录 + +- [概述](#概述) +- [安装](#安装) +- [快速开始](#快速开始) +- [类型系统](#类型系统) +- [Schema 管理](#schema-管理) +- [数据操作](#数据操作) +- [查询 API](#查询-api) +- [Scan 方法](#scan-方法) +- [Object 和 Array 类型](#object-和-array-类型) +- [索引](#索引) +- [事务和并发](#事务和并发) +- [性能优化](#性能优化) +- [错误处理](#错误处理) +- [最佳实践](#最佳实践) +- [架构细节](#架构细节) + +--- + +## 概述 + +SRDB (Simple Row Database) 是一个用 Go 编写的高性能嵌入式数据库,采用 LSM-Tree 架构,专为时序数据和高并发写入场景设计。 + +### 核心特性 + +- **高性能写入** - 基于 WAL + MemTable,支持 200K+ 写入/秒 +- **灵活的 Schema** - 支持 21 种数据类型,包括复杂类型(Object、Array) +- **强大的查询** - 链式 API,支持 18 种操作符和复合条件 +- **智能 Scan** - 自动扫描到结构体,完整支持复杂类型 +- **自动 Compaction** - 后台智能合并,优化存储空间 +- **索引支持** - 二级索引加速查询 +- **MVCC** - 多版本并发控制,无锁读 + +### 适用场景 + +- 时序数据存储(日志、指标、事件) +- 嵌入式数据库(单机应用) +- 高并发写入场景 +- 需要复杂数据类型的场景(JSON 风格数据) + +--- + +## 安装 + +```bash +go get code.tczkiot.com/wlw/srdb +``` + +**最低要求**: +- Go 1.21+ +- 支持平台:Linux、macOS、Windows + +--- + +## 快速开始 + +### 基本使用流程 + +```go +package main + +import ( + "fmt" + "log" + "code.tczkiot.com/wlw/srdb" +) + +func main() { + // 1. 打开数据库 + db, err := srdb.Open("./data") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + // 2. 定义 Schema + schema, err := srdb.NewSchema("users", []srdb.Field{ + {Name: "id", Type: srdb.Uint32, Indexed: true, Comment: "用户ID"}, + {Name: "name", Type: srdb.String, Comment: "用户名"}, + {Name: "email", Type: srdb.String, Indexed: true, Comment: "邮箱"}, + {Name: "age", Type: srdb.Int32, Comment: "年龄"}, + {Name: "settings", Type: srdb.Object, Comment: "设置(map)"}, + {Name: "tags", Type: srdb.Array, Comment: "标签(slice)"}, + }) + if err != nil { + log.Fatal(err) + } + + // 3. 创建表 + table, err := db.CreateTable("users", schema) + if err != nil { + log.Fatal(err) + } + + // 4. 插入数据 + err = table.Insert(map[string]any{ + "id": uint32(1), + "name": "Alice", + "email": "alice@example.com", + "age": int32(25), + "settings": map[string]any{ + "theme": "dark", + "lang": "zh-CN", + }, + "tags": []any{"golang", "database"}, + }) + if err != nil { + log.Fatal(err) + } + + // 5. 查询数据 + var users []User + err = table.Query(). + Eq("name", "Alice"). + Gte("age", 18). + Scan(&users) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Found %d users\n", len(users)) +} + +type User struct { + ID uint32 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Age int32 `json:"age"` + Settings map[string]string `json:"settings"` + Tags []string `json:"tags"` +} +``` + +--- + +## 类型系统 + +SRDB 支持 **21 种数据类型**,精确映射到 Go 的基础类型。 + +### 整数类型 + +#### 有符号整数(5 种) + +| 类型 | Go 类型 | 范围 | 存储大小 | +|------|---------|------|----------| +| `Int` | `int` | 平台相关 | 4/8 字节 | +| `Int8` | `int8` | -128 ~ 127 | 1 字节 | +| `Int16` | `int16` | -32,768 ~ 32,767 | 2 字节 | +| `Int32` | `int32` | -2^31 ~ 2^31-1 | 4 字节 | +| `Int64` | `int64` | -2^63 ~ 2^63-1 | 8 字节 | + +#### 无符号整数(5 种) + +| 类型 | Go 类型 | 范围 | 存储大小 | +|------|---------|------|----------| +| `Uint` | `uint` | 平台相关 | 4/8 字节 | +| `Uint8` | `uint8` | 0 ~ 255 | 1 字节 | +| `Uint16` | `uint16` | 0 ~ 65,535 | 2 字节 | +| `Uint32` | `uint32` | 0 ~ 2^32-1 | 4 字节 | +| `Uint64` | `uint64` | 0 ~ 2^64-1 | 8 字节 | + +### 浮点数类型(2 种) + +| 类型 | Go 类型 | 精度 | 存储大小 | +|------|---------|------|----------| +| `Float32` | `float32` | 单精度 | 4 字节 | +| `Float64` | `float64` | 双精度 | 8 字节 | + +### 基础类型(4 种) + +| 类型 | Go 类型 | 说明 | 存储大小 | +|------|---------|------|----------| +| `String` | `string` | UTF-8 字符串 | 变长 | +| `Bool` | `bool` | 布尔值 | 1 字节 | +| `Byte` | `byte` | 字节(uint8 别名) | 1 字节 | +| `Rune` | `rune` | Unicode 字符(int32 别名) | 4 字节 | + +### 特殊类型(2 种) + +| 类型 | Go 类型 | 说明 | 依赖 | +|------|---------|------|------| +| `Time` | `time.Time` | 时间戳 | 标准库 | +| `Decimal` | `decimal.Decimal` | 高精度十进制 | shopspring/decimal | + +### 复杂类型(2 种) + +| 类型 | Go 类型 | 说明 | 编码 | +|------|---------|------|------| +| `Object` | `map[string]xxx`, `struct{}`, `*struct{}` | JSON 对象 | JSON | +| `Array` | `[]xxx` | 数组/切片 | JSON | + +### 类型选择建议 + +```go +// ✓ 推荐:根据数据范围选择合适的类型 +type Sensor struct { + DeviceID uint32 `srdb:"device_id"` // 0 ~ 42亿 + Temperature float32 `srdb:"temperature"` // 单精度足够 + Humidity uint8 `srdb:"humidity"` // 0-100 + Status bool `srdb:"status"` // 布尔状态 +} + +// ✗ 避免:盲目使用大类型 +type Sensor struct { + DeviceID int64 // 浪费 4 字节 + Temperature float64 // 浪费 4 字节 + Humidity int64 // 浪费 7 字节! + Status int64 // 浪费 7 字节! +} +``` + +### 类型转换规则 + +SRDB 在插入数据时会进行智能类型转换: + +1. **相同类型** - 直接接受 +2. **兼容类型** - 自动转换(如 `int` → `int32`) +3. **类型提升** - 整数 → 浮点(如 `int32(42)` → `float64(42.0)`) +4. **JSON 兼容** - `float64` → 整数(需为整数值,用于 JSON 反序列化) +5. **负数检查** - 负数不能转为无符号类型 + +```go +// 示例:类型转换 +schema, _ := srdb.NewSchema("test", []srdb.Field{ + {Name: "count", Type: srdb.Int64}, + {Name: "ratio", Type: srdb.Float32}, +}) + +// ✓ 允许 +table.Insert(map[string]any{ + "count": uint32(100), // uint32 → int64 + "ratio": int32(42), // int32 → float32 (42.0) +}) + +// ✗ 拒绝 +table.Insert(map[string]any{ + "count": int32(-1), // 负数不能转为 uint +}) +``` + +--- + +## Schema 管理 + +### 创建 Schema + +#### 方式 1:手动定义 + +```go +schema, err := srdb.NewSchema("users", []srdb.Field{ + { + Name: "id", + Type: srdb.Uint32, + Indexed: true, + Nullable: false, + Comment: "用户ID", + }, + { + Name: "name", + Type: srdb.String, + Indexed: false, + Nullable: false, + Comment: "用户名", + }, + { + Name: "email", + Type: srdb.String, + Indexed: true, + Nullable: true, + Comment: "邮箱(可选)", + }, +}) +``` + +#### 方式 2:从结构体自动生成 + +```go +type User struct { + ID uint32 `srdb:"field:id;indexed;comment:用户ID"` + Name string `srdb:"field:name;comment:用户名"` + Email *string `srdb:"field:email;indexed;comment:邮箱(可选)"` + Age *int32 `srdb:"field:age;comment:年龄(可选)"` +} + +fields, err := srdb.StructToFields(User{}) +if err != nil { + log.Fatal(err) +} + +schema, err := srdb.NewSchema("users", fields) +if err != nil { + log.Fatal(err) +} +``` + +### Field 结构 + +```go +type Field struct { + Name string // 字段名(必填) + Type FieldType // 字段类型(必填) + Indexed bool // 是否创建索引 + Nullable bool // 是否允许 NULL(指针类型自动推断) + Comment string // 字段注释 +} +``` + +### Schema Tag 语法 + +```go +`srdb:"field:字段名;indexed;nullable;comment:注释"` +``` + +**支持的选项**: +- `field:name` - 指定字段名(默认使用 snake_case) +- `indexed` - 创建索引 +- `nullable` - 允许 NULL(仅用于指针类型) +- `comment:文本` - 字段注释 + +**示例**: + +```go +type User struct { + // 基本字段 + ID uint32 `srdb:"field:id;indexed;comment:用户ID"` + Name string `srdb:"field:name;comment:用户名"` + + // Nullable 字段(使用指针) + Email *string `srdb:"field:email;indexed;comment:邮箱(可选)"` + Phone *string `srdb:"field:phone;comment:手机号(可选)"` + + // 复杂类型 + Settings map[string]string `srdb:"field:settings;comment:设置"` + Tags []string `srdb:"field:tags;comment:标签"` + + // 忽略字段 + Internal string `srdb:"-"` +} +``` + +### Schema 验证 + +Schema 在创建时会进行严格验证: + +1. **字段名唯一性** - 不能重复 +2. **类型有效性** - 必须是支持的类型 +3. **Nullable 规则** - 只有指针类型可以标记 nullable +4. **保留字段** - 不能使用 `_seq`, `_time` 等保留字段 + +```go +// ✗ 错误示例 +schema, err := srdb.NewSchema("test", []srdb.Field{ + {Name: "id", Type: srdb.String}, + {Name: "id", Type: srdb.Int64}, // 错误:字段名重复 +}) + +// ✗ 错误示例 +schema, err := srdb.NewSchema("test", []srdb.Field{ + {Name: "email", Type: srdb.String, Nullable: true}, // 错误:非指针类型不能 nullable +}) +``` + +--- + +## 数据操作 + +### 插入数据 + +```go +// 单条插入 +err := table.Insert(map[string]any{ + "id": uint32(1), + "name": "Alice", + "email": "alice@example.com", + "age": int32(25), +}) + +// 批量插入 +users := []map[string]any{ + {"id": uint32(1), "name": "Alice", "age": int32(25)}, + {"id": uint32(2), "name": "Bob", "age": int32(30)}, + {"id": uint32(3), "name": "Charlie", "age": int32(35)}, +} + +for _, user := range users { + if err := table.Insert(user); err != nil { + log.Printf("插入失败: %v", err) + } +} +``` + +**注意事项**: +- 插入的数据会立即写入 WAL +- 字段类型会自动验证和转换 +- 缺失的 nullable 字段会设为 NULL +- 缺失的非 nullable 字段会报错 + +### 获取数据 + +```go +// 通过序列号获取 +row, err := table.Get(seq) +if err != nil { + log.Fatal(err) +} + +fmt.Println(row.Seq) // 序列号 +fmt.Println(row.Time) // 时间戳 +fmt.Println(row.Data) // 数据 (map[string]any) +``` + +### 更新数据 + +SRDB 是 **append-only** 架构,更新操作会创建新版本: + +```go +// 更新数据 +err := table.Update(seq, map[string]any{ + "age": int32(26), +}) + +// 等价于: +newData := existingData +newData["age"] = int32(26) +table.Insert(newData) +``` + +### 删除数据 + +```go +// 标记删除(软删除) +err := table.Delete(seq) + +// 物理删除在 Compaction 时进行 +``` + +--- + +## 查询 API + +SRDB 提供流畅的链式查询 API。 + +### 基本查询 + +```go +// 等值查询 +rows, err := table.Query().Eq("name", "Alice").Rows() + +// 不等于 +rows, err := table.Query().NotEq("status", "deleted").Rows() + +// 大于/小于 +rows, err := table.Query(). + Gt("age", 18). + Lt("age", 60). + Rows() + +// 大于等于/小于等于 +rows, err := table.Query(). + Gte("score", 60). + Lte("score", 100). + Rows() +``` + +### 集合查询 + +```go +// IN +rows, err := table.Query(). + In("status", []any{"active", "pending", "processing"}). + Rows() + +// NOT IN +rows, err := table.Query(). + NotIn("role", []any{"banned", "suspended"}). + Rows() + +// BETWEEN +rows, err := table.Query(). + Between("age", 18, 60). + Rows() + +// NOT BETWEEN +rows, err := table.Query(). + NotBetween("price", 1000, 5000). + Rows() +``` + +### 字符串查询 + +```go +// 包含子串 +rows, err := table.Query().Contains("message", "error").Rows() + +// 不包含 +rows, err := table.Query().NotContains("message", "debug").Rows() + +// 前缀匹配 +rows, err := table.Query().StartsWith("email", "admin@").Rows() + +// 后缀匹配 +rows, err := table.Query().EndsWith("filename", ".log").Rows() +``` + +### NULL 查询 + +```go +// IS NULL +rows, err := table.Query().IsNull("email").Rows() + +// IS NOT NULL +rows, err := table.Query().NotNull("phone").Rows() +``` + +### 复合条件 + +```go +// AND(默认) +rows, err := table.Query(). + Eq("status", "active"). + Gte("age", 18). + NotNull("email"). + Rows() + +// OR +rows, err := table.Query(). + Where(srdb.Or( + srdb.Eq("role", "admin"), + srdb.Eq("role", "moderator"), + )). + Rows() + +// 复杂组合 +rows, err := table.Query(). + Where(srdb.And( + srdb.Eq("status", "active"), + srdb.Or( + srdb.Gte("age", 18), + srdb.Eq("verified", true), + ), + srdb.Not(srdb.Eq("role", "banned")), + )). + Rows() +``` + +### 字段选择 + +```go +// 只查询指定字段(性能优化) +rows, err := table.Query(). + Select("id", "name", "email"). + Eq("status", "active"). + Rows() + +// 遍历结果 +for rows.Next() { + row := rows.Row() + data := row.Data() // 只包含 id, name, email + fmt.Println(data) +} +``` + +### 结果获取 + +```go +// 游标模式(惰性加载,推荐) +rows, err := table.Query().Rows() +defer rows.Close() + +for rows.Next() { + row := rows.Row() + fmt.Println(row.Data()) +} + +// 检查错误 +if err := rows.Err(); err != nil { + log.Fatal(err) +} + +// 获取第一条 +row, err := table.Query().First() + +// 获取最后一条 +row, err := table.Query().Last() + +// 收集所有结果(内存消耗大) +data := rows.Collect() + +// 获取总数 +count := rows.Count() +``` + +### 操作符完整列表 + +| 方法 | 操作符 | 说明 | 示例 | +|------|--------|------|------| +| `Eq(field, value)` | `=` | 等于 | `.Eq("status", "active")` | +| `NotEq(field, value)` | `!=` | 不等于 | `.NotEq("role", "guest")` | +| `Lt(field, value)` | `<` | 小于 | `.Lt("age", 18)` | +| `Gt(field, value)` | `>` | 大于 | `.Gt("score", 60)` | +| `Lte(field, value)` | `<=` | 小于等于 | `.Lte("price", 100)` | +| `Gte(field, value)` | `>=` | 大于等于 | `.Gte("count", 10)` | +| `In(field, values)` | `IN` | 在列表中 | `.In("status", []any{"a", "b"})` | +| `NotIn(field, values)` | `NOT IN` | 不在列表中 | `.NotIn("role", []any{"banned"})` | +| `Between(field, min, max)` | `BETWEEN` | 在范围内 | `.Between("age", 18, 60)` | +| `NotBetween(field, min, max)` | `NOT BETWEEN` | 不在范围内 | `.NotBetween("price", 0, 10)` | +| `Contains(field, pattern)` | `CONTAINS` | 包含子串 | `.Contains("message", "error")` | +| `NotContains(field, pattern)` | `NOT CONTAINS` | 不包含 | `.NotContains("log", "debug")` | +| `StartsWith(field, prefix)` | `STARTS WITH` | 以...开头 | `.StartsWith("email", "admin")` | +| `NotStartsWith(field, prefix)` | `NOT STARTS WITH` | 不以...开头 | `.NotStartsWith("name", "test")` | +| `EndsWith(field, suffix)` | `ENDS WITH` | 以...结尾 | `.EndsWith("file", ".log")` | +| `NotEndsWith(field, suffix)` | `NOT ENDS WITH` | 不以...结尾 | `.NotEndsWith("path", ".tmp")` | +| `IsNull(field)` | `IS NULL` | 为空 | `.IsNull("email")` | +| `NotNull(field)` | `IS NOT NULL` | 不为空 | `.NotNull("phone")` | + +--- + +## Scan 方法 + +SRDB 提供智能的 Scan 方法,可以将查询结果直接扫描到 Go 结构体。 + +### Row.Scan() - 扫描单行 + +```go +row, err := table.Query().Eq("id", 1).First() +if err != nil { + log.Fatal(err) +} + +var user User +err = row.Scan(&user) +if err != nil { + log.Fatal(err) +} + +fmt.Println(user.Name) // "Alice" +``` + +### Rows.Scan() - 智能扫描 + +**Rows.Scan 会自动判断目标类型**: +- 如果目标是**切片** → 扫描所有行 +- 如果目标是**结构体** → 只扫描第一行 + +```go +// 扫描多行到切片 +rows, _ := table.Query().Rows() +defer rows.Close() + +var users []User +err := rows.Scan(&users) + +// 扫描单行到结构体(智能判断) +rows2, _ := table.Query().Eq("id", 1).Rows() +defer rows2.Close() + +var user User +err := rows2.Scan(&user) // 自动只扫描第一行 +``` + +### QueryBuilder.Scan() - 最简洁的方式 + +```go +// 扫描多行 +var users []User +err := table.Query().Scan(&users) + +// 扫描单行 +var user User +err := table.Query().Eq("id", 1).Scan(&user) + +// 带条件扫描 +var activeUsers []User +err := table.Query(). + Eq("status", "active"). + Gte("age", 18). + Scan(&activeUsers) +``` + +### 部分字段扫描 + +```go +// 定义简化的结构体 +type UserBrief struct { + Name string `json:"name"` + Email string `json:"email"` +} + +// 只扫描指定字段 +var briefs []UserBrief +err := table.Query(). + Select("name", "email"). + Scan(&briefs) + +// 结果只包含 name 和 email 字段 +``` + +### 复杂类型扫描 + +```go +type User struct { + Name string `json:"name"` + Email string `json:"email"` + Settings map[string]string `json:"settings"` // Object + Tags []string `json:"tags"` // Array + Metadata map[string]any `json:"metadata"` // Object with any + Scores []int `json:"scores"` // Array of int +} + +var user User +err := table.Query().Eq("name", "Alice").Scan(&user) + +// 访问复杂类型 +fmt.Println(user.Settings["theme"]) // "dark" +fmt.Println(user.Tags[0]) // "golang" +fmt.Println(user.Metadata["version"]) // "1.0" +fmt.Println(user.Scores[0]) // 95 +``` + +### Scan 的工作原理 + +1. **Row.Scan**: + - 使用 `json.Marshal` 将 row.Data() 转为 JSON + - 使用 `json.Unmarshal` 解码到目标结构体 + - 应用字段过滤(如果调用了 Select) + +2. **Rows.Scan**: + - 使用 `reflect` 检查目标类型 + - 如果是切片:调用 Collect() 获取所有行,然后 JSON 转换 + - 如果是结构体:调用 First() 获取第一行,然后调用 Row.Scan + +3. **QueryBuilder.Scan**: + - 直接调用 Rows.Scan + +--- + +## Object 和 Array 类型 + +SRDB 原生支持复杂的数据类型,可以存储 JSON 风格的对象和数组。 + +### Object 类型 + +Object 类型可以存储: +- `map[string]string` +- `map[string]any` +- `struct{}` +- `*struct{}` + +#### 定义 Object 字段 + +```go +type User struct { + Settings map[string]string `srdb:"field:settings"` + Metadata map[string]any `srdb:"field:metadata"` +} + +// 或手动定义 +schema, _ := srdb.NewSchema("users", []srdb.Field{ + {Name: "settings", Type: srdb.Object, Comment: "用户设置"}, + {Name: "metadata", Type: srdb.Object, Comment: "元数据"}, +}) +``` + +#### 插入 Object 数据 + +```go +err := table.Insert(map[string]any{ + "name": "Alice", + "settings": map[string]any{ + "theme": "dark", + "language": "zh-CN", + "fontSize": "14px", + }, + "metadata": map[string]any{ + "version": "1.0", + "author": "Alice", + "tags": []string{"admin", "verified"}, // 嵌套数组 + }, +}) +``` + +#### 查询和使用 Object + +```go +var user User +table.Query().Eq("name", "Alice").Scan(&user) + +// 访问 Object 字段 +theme := user.Settings["theme"] // "dark" +version := user.Metadata["version"] // "1.0" + +// 类型断言(for map[string]any) +if tags, ok := user.Metadata["tags"].([]any); ok { + fmt.Println(tags[0]) // "admin" +} +``` + +### Array 类型 + +Array 类型可以存储任意切片: +- `[]string` +- `[]int` +- `[]any` +- `[]struct{}` + +#### 定义 Array 字段 + +```go +type User struct { + Tags []string `srdb:"field:tags"` + Scores []int `srdb:"field:scores"` + Items []any `srdb:"field:items"` +} + +// 或手动定义 +schema, _ := srdb.NewSchema("users", []srdb.Field{ + {Name: "tags", Type: srdb.Array, Comment: "标签"}, + {Name: "scores", Type: srdb.Array, Comment: "分数"}, +}) +``` + +#### 插入 Array 数据 + +```go +err := table.Insert(map[string]any{ + "name": "Alice", + "tags": []any{"golang", "database", "lsm-tree"}, + "scores": []any{95, 88, 92}, + "items": []any{ + "item1", + 123, + true, + map[string]any{"nested": "value"}, // 嵌套对象 + }, +}) +``` + +#### 查询和使用 Array + +```go +var user User +table.Query().Eq("name", "Alice").Scan(&user) + +// 访问 Array 字段 +fmt.Println(len(user.Tags)) // 3 +fmt.Println(user.Tags[0]) // "golang" +fmt.Println(user.Scores[1]) // 88 + +// 遍历 +for _, tag := range user.Tags { + fmt.Println(tag) +} + +// 计算平均分 +total := 0 +for _, score := range user.Scores { + total += score +} +avg := float64(total) / float64(len(user.Scores)) +``` + +### 嵌套结构 + +Object 和 Array 可以任意嵌套: + +```go +type Config struct { + Server string `json:"server"` + Port int `json:"port"` + Features map[string]bool `json:"features"` // 嵌套 Object +} + +type Application struct { + Name string `json:"name"` + Config Config `json:"config"` // 嵌套结构体 + Servers []string `json:"servers"` // Array + Tags []string `json:"tags"` // Array + Meta map[string]any `json:"meta"` // Object +} + +// 插入嵌套数据 +table.Insert(map[string]any{ + "name": "MyApp", + "config": map[string]any{ + "server": "localhost", + "port": 8080, + "features": map[string]any{ + "cache": true, + "logging": false, + }, + }, + "servers": []any{"server1", "server2", "server3"}, + "tags": []any{"production", "v1.0"}, + "meta": map[string]any{ + "deployedAt": time.Now().Format(time.RFC3339), + "region": "us-west", + "replicas": 3, + }, +}) + +// 查询和访问 +var app Application +table.Query().Eq("name", "MyApp").Scan(&app) + +fmt.Println(app.Config.Server) // "localhost" +fmt.Println(app.Config.Features["cache"]) // true +fmt.Println(app.Servers[0]) // "server1" +fmt.Println(app.Meta["region"]) // "us-west" +``` + +### 空值处理 + +```go +// 插入空 Object 和 Array +table.Insert(map[string]any{ + "name": "Charlie", + "settings": map[string]any{}, // 空 Object + "tags": []any{}, // 空 Array +}) + +// 查询 +var user User +table.Query().Eq("name", "Charlie").Scan(&user) + +// 安全检查 +if len(user.Settings) == 0 { + fmt.Println("设置为空") +} + +if len(user.Tags) == 0 { + fmt.Println("没有标签") +} +``` + +### 存储格式 + +- **编码方式**:JSON +- **存储格式**:`[length: uint32][JSON data]` +- **零值**:Object 为 `{}`,Array 为 `[]` +- **性能**:JSON 编码/解码有一定开销,但保证了灵活性 + +--- + +## 索引 + +SRDB 支持二级索引,可以显著加速查询性能。 + +### 创建索引 + +```go +// 在 Schema 中标记索引 +schema, _ := srdb.NewSchema("users", []srdb.Field{ + {Name: "id", Type: srdb.Uint32, Indexed: true}, // 创建索引 + {Name: "email", Type: srdb.String, Indexed: true}, // 创建索引 + {Name: "name", Type: srdb.String, Indexed: false}, // 不创建索引 +}) +``` + +### 索引的工作原理 + +1. **自动创建**:创建表时,所有标记为 `Indexed: true` 的字段会自动创建索引 +2. **自动更新**:插入/更新数据时,索引会自动更新 +3. **查询优化**:使用 `Eq()` 查询索引字段时,会自动使用索引 + +```go +// 使用索引(快速) +rows, _ := table.Query().Eq("email", "alice@example.com").Rows() + +// 不使用索引(全表扫描) +rows, _ := table.Query().Contains("name", "Alice").Rows() +``` + +### 索引适用场景 + +**适合创建索引**: +- ✅ 经常用于等值查询的字段(`Eq`) +- ✅ 高基数字段(unique 或接近 unique) +- ✅ 查询频繁的字段 + +**不适合创建索引**: +- ❌ 低基数字段(如性别、状态等) +- ❌ 很少查询的字段 +- ❌ 频繁更新的字段 +- ❌ Object 和 Array 类型字段 + +### 索引性能 + +| 操作 | 无索引 | 有索引 | 提升 | +|------|--------|--------|------| +| 等值查询 (Eq) | O(N) | O(log N) | ~1000x | +| 范围查询 (Gt/Lt) | O(N) | O(N) | 无提升 | +| 模糊查询 (Contains) | O(N) | O(N) | 无提升 | + +--- + +## 事务和并发 + +### 并发控制 + +SRDB 使用 **MVCC (多版本并发控制)** 实现无锁并发读写: + +- **写入**:追加到 WAL 和 MemTable,使用互斥锁保护 +- **读取**:无锁读取,读取的是快照版本 +- **Compaction**:后台异步执行,不阻塞读写 + +```go +// 多个 goroutine 并发写入 +var wg sync.WaitGroup +for i := 0; i < 100; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + table.Insert(map[string]any{ + "id": uint32(id), + "name": fmt.Sprintf("user_%d", id), + }) + }(i) +} +wg.Wait() + +// 多个 goroutine 并发读取 +for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + rows, _ := table.Query().Rows() + defer rows.Close() + for rows.Next() { + _ = rows.Row() + } + }() +} +wg.Wait() +``` + +### 事务支持 + +⚠️ **当前版本不支持显式事务**,但保证: +- 单条写入的原子性(通过 WAL) +- 数据持久性(WAL fsync) +- 崩溃恢复(WAL 重放) + +未来版本计划支持: +- [ ] 显式事务 API +- [ ] 批量操作的原子性 +- [ ] ACID 保证 + +--- + +## 性能优化 + +### 写入优化 + +**1. 批量写入** + +```go +// ✓ 好:批量写入,减少 fsync 次数 +for i := 0; i < 1000; i++ { + table.Insert(data[i]) +} + +// ✗ 避免:每次都打开关闭数据库 +for i := 0; i < 1000; i++ { + db, _ := srdb.Open("./data") + table.Insert(data[i]) + db.Close() +} +``` + +**2. 调整 MemTable 大小** + +```go +// 默认 64MB,可以根据内存调整 +// 更大的 MemTable = 更少的 flush,但占用更多内存 +``` + +### 查询优化 + +**1. 使用索引** + +```go +// ✓ 好:使用索引字段查询 +rows, _ := table.Query().Eq("email", "alice@example.com").Rows() + +// ✗ 避免:全表扫描 +rows, _ := table.Query().Contains("name", "Alice").Rows() +``` + +**2. 字段选择** + +```go +// ✓ 好:只查询需要的字段 +rows, _ := table.Query().Select("id", "name").Rows() + +// ✗ 避免:查询所有字段 +rows, _ := table.Query().Rows() +``` + +**3. 使用游标模式** + +```go +// ✓ 好:惰性加载,节省内存 +rows, _ := table.Query().Rows() +defer rows.Close() +for rows.Next() { + process(rows.Row()) +} + +// ✗ 避免:一次性加载所有数据 +data := rows.Collect() // 内存消耗大 +``` + +### 存储优化 + +**1. 定期 Compaction** + +Compaction 会自动触发,但可以手动触发: + +```go +// 手动触发 Compaction(阻塞) +err := table.Compact() +``` + +**2. 选择合适的类型** + +```go +// ✓ 好:根据数据范围选择类型 +type Sensor struct { + DeviceID uint32 // 0 ~ 42亿,4字节 + Value float32 // 单精度,4字节 +} + +// ✗ 避免:使用过大的类型 +type Sensor struct { + DeviceID int64 // 8字节,浪费4字节 + Value float64 // 8字节,浪费4字节 +} +``` + +### 内存优化 + +**1. 及时关闭游标** + +```go +// ✓ 好:使用 defer 确保关闭 +rows, _ := table.Query().Rows() +defer rows.Close() + +// ✗ 避免:忘记关闭 +rows, _ := table.Query().Rows() +// ... 使用 rows +// 忘记调用 rows.Close() +``` + +**2. 避免大量缓存** + +```go +// ✗ 避免:缓存大量数据 +var cache []map[string]any +rows, _ := table.Query().Rows() +cache = rows.Collect() // 内存消耗大 + +// ✓ 好:流式处理 +rows, _ := table.Query().Rows() +defer rows.Close() +for rows.Next() { + process(rows.Row()) // 逐条处理 +} +``` + +--- + +## 错误处理 + +SRDB 使用统一的错误码系统。 + +### 错误类型 + +```go +// 创建错误 +err := srdb.NewError(srdb.ErrCodeTableNotFound, nil) + +// 包装错误 +err := srdb.WrapError(baseErr, "failed to insert: %v", data) + +// 判断错误类型 +if srdb.IsNotFound(err) { + // 处理未找到错误 +} + +if srdb.IsCorrupted(err) { + // 处理数据损坏错误 +} +``` + +### 常见错误码 + +| 错误码 | 说明 | 处理方式 | +|--------|------|----------| +| `ErrCodeNotFound` | 数据不存在 | 检查 key 是否正确 | +| `ErrCodeTableNotFound` | 表不存在 | 先创建表 | +| `ErrCodeSchemaValidation` | Schema 验证失败 | 检查字段定义 | +| `ErrCodeTypeConversion` | 类型转换失败 | 检查数据类型 | +| `ErrCodeCorrupted` | 数据损坏 | 恢复备份或重建 | +| `ErrCodeClosed` | 数据库已关闭 | 重新打开数据库 | + +### 错误处理最佳实践 + +```go +// ✓ 好:检查并处理错误 +if err := table.Insert(data); err != nil { + if srdb.IsSchemaValidation(err) { + log.Printf("数据验证失败: %v", err) + return + } + log.Printf("插入失败: %v", err) + return +} + +// ✗ 避免:忽略错误 +table.Insert(data) // 错误未处理 +``` + +--- + +## 最佳实践 + +### Schema 设计 + +1. **选择合适的类型** + ```go + // ✓ 根据数据范围选择 + DeviceID uint32 // 0 ~ 42亿 + Count uint8 // 0 ~ 255 + ``` + +2. **合理使用索引** + ```go + // ✓ 高基数、频繁查询的字段 + Email string `srdb:"indexed"` + + // ✗ 低基数字段不需要索引 + Gender string // 只有 2-3 个值 + ``` + +3. **Nullable 字段使用指针** + ```go + Email *string `srdb:"field:email"` + Phone *string `srdb:"field:phone"` + ``` + +### 数据插入 + +1. **批量插入** + ```go + for _, data := range batch { + table.Insert(data) + } + ``` + +2. **验证数据** + ```go + if email == "" { + return errors.New("email required") + } + table.Insert(data) + ``` + +### 查询优化 + +1. **使用索引字段** + ```go + // ✓ 使用索引 + table.Query().Eq("email", "alice@example.com") + + // ✗ 避免全表扫描 + table.Query().Contains("email", "@example.com") + ``` + +2. **字段选择** + ```go + table.Query().Select("id", "name").Rows() + ``` + +3. **使用 Scan** + ```go + var users []User + table.Query().Scan(&users) + ``` + +### 并发访问 + +1. **读写分离** + ```go + // 多个 goroutine 可以安全并发读 + go func() { + table.Query().Rows() + }() + ``` + +2. **写入控制** + ```go + // 写入使用队列控制并发 + ``` + +--- + +## 架构细节 + +### LSM-Tree 结构 + +``` +写入流程: +数据 → WAL(持久化)→ MemTable → Immutable MemTable → Level 0 SST → Compaction → Level 1-6 +``` + +### 文件组织 + +``` +database_dir/ +├── database.meta # 数据库元数据 +├── MANIFEST # 版本控制 +└── table_name/ + ├── schema.json # 表 Schema + ├── MANIFEST # 表级版本控制 + ├── 000001.wal # WAL 文件 + ├── 000001.sst # SST 文件 + ├── 000002.sst + └── idx_email.sst # 索引文件 +``` + +### Compaction 策略 + +- **Level 0**: 文件数量 ≥ 4 触发 +- **Level 1-6**: 总大小超过阈值触发 +- **Score 计算**: `size / max_size` 或 `file_count / max_files` +- **文件大小**: L0=2MB, L1=10MB, L2=50MB, L3=100MB, L4+=200MB + +### 性能指标 + +| 操作 | 性能 | +|------|------| +| 顺序写入 | ~100K ops/s | +| 随机写入 | ~50K ops/s | +| 点查询 | ~10K ops/s | +| 范围扫描 | ~1M rows/s | +| 内存使用 | < 150MB (64MB MemTable + overhead) | + +--- + +## 附录 + +### 参考链接 + +- [GitHub 仓库](https://code.tczkiot.com/wlw/srdb) +- [API 文档](https://pkg.go.dev/code.tczkiot.com/wlw/srdb) +- [设计文档](DESIGN.md) +- [开发者指南](CLAUDE.md) + +### 示例项目 + +- [所有类型示例](examples/all_types/) +- [Scan 方法示例](examples/scan_demo/) +- [Nullable 示例](examples/nullable/) +- [Web UI](examples/webui/) + +### 许可证 + +MIT License - 详见 [LICENSE](LICENSE) 文件 + +--- + +**SRDB** - 简单、高效、可靠的嵌入式数据库 🚀 diff --git a/README.md b/README.md index 87fd27c..13e3204 100644 --- a/README.md +++ b/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 字段的使用 + --- ## 🤝 贡献 diff --git a/examples/all_types/data/sensors/schema.json b/examples/all_types/data/sensors/schema.json deleted file mode 100644 index feb8f13..0000000 --- a/examples/all_types/data/sensors/schema.json +++ /dev/null @@ -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": "是否在线" - } - ] - } -} \ No newline at end of file diff --git a/examples/all_types/data/sensors/wal/CURRENT b/examples/all_types/data/sensors/wal/CURRENT deleted file mode 100644 index 0cfbf08..0000000 --- a/examples/all_types/data/sensors/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/examples/all_types/main.go b/examples/all_types/main.go deleted file mode 100644 index 90bcbd4..0000000 --- a/examples/all_types/main.go +++ /dev/null @@ -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 (语义清晰)") -} diff --git a/examples/batch_insert/README.md b/examples/batch_insert/README.md deleted file mode 100644 index ae41129..0000000 --- a/examples/batch_insert/README.md +++ /dev/null @@ -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 示例 diff --git a/examples/batch_insert/data/example1/MANIFEST-000001 b/examples/batch_insert/data/example1/MANIFEST-000001 deleted file mode 100644 index da26eca508432583a42610606e21f5abe3899277..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq`NYemu`n|* q2b$`endhBa3AVsDu>!^n$}e}$FU>0fiq{h5K4WzE<(1~-)B*q{JWj0u diff --git a/examples/batch_insert/data/example1/schema.json b/examples/batch_insert/data/example1/schema.json deleted file mode 100644 index 3da11d1..0000000 --- a/examples/batch_insert/data/example1/schema.json +++ /dev/null @@ -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": "年龄" - } - ] - } -} \ No newline at end of file diff --git a/examples/batch_insert/data/example1/wal/CURRENT b/examples/batch_insert/data/example1/wal/CURRENT deleted file mode 100644 index 0cfbf08..0000000 --- a/examples/batch_insert/data/example1/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/examples/batch_insert/data/example2/MANIFEST-000001 b/examples/batch_insert/data/example2/MANIFEST-000001 deleted file mode 100644 index 7551134fcccec8610e04ec5a090a6dd78f5f6646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq`iV(^{h3XN>N?ywaSUS^y*CPOks} diff --git a/examples/batch_insert/data/example2/schema.json b/examples/batch_insert/data/example2/schema.json deleted file mode 100644 index d81fb40..0000000 --- a/examples/batch_insert/data/example2/schema.json +++ /dev/null @@ -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": "" - } - ] - } -} \ No newline at end of file diff --git a/examples/batch_insert/data/example2/wal/CURRENT b/examples/batch_insert/data/example2/wal/CURRENT deleted file mode 100644 index 0cfbf08..0000000 --- a/examples/batch_insert/data/example2/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/examples/batch_insert/data/example3/CURRENT b/examples/batch_insert/data/example3/CURRENT deleted file mode 100644 index 7ed683d..0000000 --- a/examples/batch_insert/data/example3/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/examples/batch_insert/data/example3/MANIFEST-000001 b/examples/batch_insert/data/example3/MANIFEST-000001 deleted file mode 100644 index d26da34b438dba1a6bea979ec07bb1fcc87540d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq`wFE`PFSVisq`0{)pt%d diff --git a/examples/batch_insert/data/example4/schema.json b/examples/batch_insert/data/example4/schema.json deleted file mode 100644 index a2cf2c8..0000000 --- a/examples/batch_insert/data/example4/schema.json +++ /dev/null @@ -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": "" - } - ] - } -} \ No newline at end of file diff --git a/examples/batch_insert/data/example4/wal/CURRENT b/examples/batch_insert/data/example4/wal/CURRENT deleted file mode 100644 index 0cfbf08..0000000 --- a/examples/batch_insert/data/example4/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/examples/batch_insert/data/example5/CURRENT b/examples/batch_insert/data/example5/CURRENT deleted file mode 100644 index 7ed683d..0000000 --- a/examples/batch_insert/data/example5/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/examples/batch_insert/data/example5/MANIFEST-000001 b/examples/batch_insert/data/example5/MANIFEST-000001 deleted file mode 100644 index cd20666e0fa7dc792073e0fd54eb0fd0445b282f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq`0{)pt%d diff --git a/examples/batch_insert/data/example5/schema.json b/examples/batch_insert/data/example5/schema.json deleted file mode 100644 index a2cf2c8..0000000 --- a/examples/batch_insert/data/example5/schema.json +++ /dev/null @@ -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": "" - } - ] - } -} \ No newline at end of file diff --git a/examples/batch_insert/data/example5/wal/CURRENT b/examples/batch_insert/data/example5/wal/CURRENT deleted file mode 100644 index 0cfbf08..0000000 --- a/examples/batch_insert/data/example5/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/examples/batch_insert/data/example6/CURRENT b/examples/batch_insert/data/example6/CURRENT deleted file mode 100644 index 7ed683d..0000000 --- a/examples/batch_insert/data/example6/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/examples/batch_insert/data/example6/MANIFEST-000001 b/examples/batch_insert/data/example6/MANIFEST-000001 deleted file mode 100644 index 8ce6e11b9f3bc4c2627bb31d3be27a240cbb75c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq`iV<65HJ2h@Y=zP!?$oLT_Mu1+2R diff --git a/examples/batch_insert/data/example6/schema.json b/examples/batch_insert/data/example6/schema.json deleted file mode 100644 index a086de4..0000000 --- a/examples/batch_insert/data/example6/schema.json +++ /dev/null @@ -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": "是否有货" - } - ] - } -} \ No newline at end of file diff --git a/examples/batch_insert/data/example6/wal/CURRENT b/examples/batch_insert/data/example6/wal/CURRENT deleted file mode 100644 index 0cfbf08..0000000 --- a/examples/batch_insert/data/example6/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/examples/batch_insert/main.go b/examples/batch_insert/main.go deleted file mode 100644 index 2ad6948..0000000 --- a/examples/batch_insert/main.go +++ /dev/null @@ -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"]) -} diff --git a/examples/new_types/README.md b/examples/new_types/README.md deleted file mode 100644 index 55b4592..0000000 --- a/examples/new_types/README.md +++ /dev/null @@ -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 类型保证金融数据精确性 diff --git a/examples/new_types/data/api_logs/CURRENT b/examples/new_types/data/api_logs/CURRENT deleted file mode 100644 index 7ed683d..0000000 --- a/examples/new_types/data/api_logs/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/examples/new_types/data/api_logs/schema.json b/examples/new_types/data/api_logs/schema.json deleted file mode 100644 index ec2e7a2..0000000 --- a/examples/new_types/data/api_logs/schema.json +++ /dev/null @@ -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": "响应时间(毫秒)" - } - ] - } -} \ No newline at end of file diff --git a/examples/new_types/data/api_logs/wal/CURRENT b/examples/new_types/data/api_logs/wal/CURRENT deleted file mode 100644 index 0cfbf08..0000000 --- a/examples/new_types/data/api_logs/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/examples/new_types/go.mod b/examples/new_types/go.mod deleted file mode 100644 index c3cdc86..0000000 --- a/examples/new_types/go.mod +++ /dev/null @@ -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 -) diff --git a/examples/new_types/go.sum b/examples/new_types/go.sum deleted file mode 100644 index 79975e3..0000000 --- a/examples/new_types/go.sum +++ /dev/null @@ -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= diff --git a/examples/new_types/main.go b/examples/new_types/main.go deleted file mode 100644 index 4bde748..0000000 --- a/examples/new_types/main.go +++ /dev/null @@ -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()) -} diff --git a/examples/nullable/README.md b/examples/nullable/README.md deleted file mode 100644 index 8ab07c5..0000000 --- a/examples/nullable/README.md +++ /dev/null @@ -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: ") - } -} -``` - -**优点**: -- ✓ 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=") - } - - if data["age"] != nil { - fmt.Printf(", age=%d", data["age"]) - } else { - fmt.Print(", age=") - } - - 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=, phone=13900139000, age= - - Charlie: email=, phone=, age= -``` diff --git a/examples/nullable/main.go b/examples/nullable/main.go deleted file mode 100644 index 6a28814..0000000 --- a/examples/nullable/main.go +++ /dev/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=") - } - - if phone := data["phone"]; phone != nil { - fmt.Printf(", phone=%s", phone) - } else { - fmt.Print(", phone=") - } - - if age := data["age"]; age != nil { - fmt.Printf(", age=%d", age) - } else { - fmt.Print(", age=") - } - - 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 习惯") -} diff --git a/examples/snake_case_demo/main.go b/examples/snake_case_demo/main.go deleted file mode 100644 index 3fb68e2..0000000 --- a/examples/snake_case_demo/main.go +++ /dev/null @@ -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)) -} diff --git a/examples/struct_schema/README.md b/examples/struct_schema/README.md deleted file mode 100644 index a9ac6eb..0000000 --- a/examples/struct_schema/README.md +++ /dev/null @@ -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 等复杂类型 diff --git a/examples/struct_schema/data/users/CURRENT b/examples/struct_schema/data/users/CURRENT deleted file mode 100644 index 7ed683d..0000000 --- a/examples/struct_schema/data/users/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/examples/struct_schema/data/users/MANIFEST-000001 b/examples/struct_schema/data/users/MANIFEST-000001 deleted file mode 100644 index 7fefdd91b226b8da86b8e7a21c1ae34c397e91cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 405 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq`pq`-oa_9Wgyb>iVqgtZeXN>N?ywaSU+Wc4MPY62Egjgp6 Q?SMHEsE1@HnqqY#0PtCY#sB~S diff --git a/examples/struct_schema/data/users/schema.json b/examples/struct_schema/data/users/schema.json deleted file mode 100644 index a2e9058..0000000 --- a/examples/struct_schema/data/users/schema.json +++ /dev/null @@ -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": "是否激活" - } - ] - } -} \ No newline at end of file diff --git a/examples/struct_schema/data/users/wal/CURRENT b/examples/struct_schema/data/users/wal/CURRENT deleted file mode 100644 index 00750ed..0000000 --- a/examples/struct_schema/data/users/wal/CURRENT +++ /dev/null @@ -1 +0,0 @@ -3 diff --git a/examples/struct_schema/main.go b/examples/struct_schema/main.go deleted file mode 100644 index d22a74e..0000000 --- a/examples/struct_schema/main.go +++ /dev/null @@ -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✓ 所有示例执行成功!") -} diff --git a/examples/struct_tags/README.md b/examples/struct_tags/README.md deleted file mode 100644 index 090a0ee..0000000 --- a/examples/struct_tags/README.md +++ /dev/null @@ -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, 邮箱: , 余额: - -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 - ``` diff --git a/examples/struct_tags/data/database.meta b/examples/struct_tags/data/database.meta deleted file mode 100644 index efd7ee3..0000000 --- a/examples/struct_tags/data/database.meta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 1, - "tables": [ - { - "name": "users", - "dir": "users", - "created_at": 1760032030 - } - ] -} \ No newline at end of file diff --git a/examples/struct_tags/data/users/CURRENT b/examples/struct_tags/data/users/CURRENT deleted file mode 100644 index 7ed683d..0000000 --- a/examples/struct_tags/data/users/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000001 diff --git a/examples/struct_tags/data/users/MANIFEST-000001 b/examples/struct_tags/data/users/MANIFEST-000001 deleted file mode 100644 index 6c53b5f00063d812c2e28fabae4cf694cbcd4c87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79 zcmZ=XS-sYsfq|h~$uT7*HN`D6C$(6~Dmqq2$t5)>wFE`PFSVisq` ../.. - -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 -) diff --git a/examples/struct_tags/go.sum b/examples/struct_tags/go.sum deleted file mode 100644 index 79975e3..0000000 --- a/examples/struct_tags/go.sum +++ /dev/null @@ -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= diff --git a/examples/struct_tags/main.go b/examples/struct_tags/main.go deleted file mode 100644 index 96a906f..0000000 --- a/examples/struct_tags/main.go +++ /dev/null @@ -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(", 邮箱: ") - } else { - fmt.Printf(", 邮箱: %v", email) - } - if balance == nil { - fmt.Printf(", 余额: ") - } 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:\"-\" # 忽略字段") -} diff --git a/examples/tag_format/README.md b/examples/tag_format/README.md deleted file mode 100644 index 8969ee1..0000000 --- a/examples/tag_format/README.md +++ /dev/null @@ -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 -- 内存开销:无额外分配 -- 向后兼容:零性能损失 diff --git a/examples/tag_format/main.go b/examples/tag_format/main.go deleted file mode 100644 index 8beb073..0000000 --- a/examples/tag_format/main.go +++ /dev/null @@ -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(" • 完全向后兼容旧格式") -} diff --git a/examples/time_duration/main.go b/examples/time_duration/main.go deleted file mode 100644 index 8b74e8f..0000000 --- a/examples/time_duration/main.go +++ /dev/null @@ -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) -} diff --git a/examples/time_duration_simple/main.go b/examples/time_duration_simple/main.go deleted file mode 100644 index 046380d..0000000 --- a/examples/time_duration_simple/main.go +++ /dev/null @@ -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) -} diff --git a/examples/webui/data/database.meta b/examples/webui/data/database.meta new file mode 100644 index 0000000..22ee73d --- /dev/null +++ b/examples/webui/data/database.meta @@ -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 + } + ] +} \ No newline at end of file diff --git a/examples/all_types/data/sensors/CURRENT b/examples/webui/data/logs/CURRENT similarity index 100% rename from examples/all_types/data/sensors/CURRENT rename to examples/webui/data/logs/CURRENT diff --git a/examples/webui/data/logs/MANIFEST-000001 b/examples/webui/data/logs/MANIFEST-000001 new file mode 100644 index 0000000000000000000000000000000000000000..4e85eb1e9bbffc52753a7acd7bf9b12a28e8351d GIT binary patch literal 63545 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 literal 0 HcmV?d00001 diff --git a/examples/webui/data/logs/schema.json b/examples/webui/data/logs/schema.json new file mode 100644 index 0000000..4d5d5ac --- /dev/null +++ b/examples/webui/data/logs/schema.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/examples/webui/data/logs/wal/CURRENT b/examples/webui/data/logs/wal/CURRENT new file mode 100644 index 0000000..1b9cba4 --- /dev/null +++ b/examples/webui/data/logs/wal/CURRENT @@ -0,0 +1 @@ +151 diff --git a/examples/batch_insert/data/example1/CURRENT b/examples/webui/data/products/CURRENT similarity index 100% rename from examples/batch_insert/data/example1/CURRENT rename to examples/webui/data/products/CURRENT diff --git a/examples/all_types/data/sensors/MANIFEST-000001 b/examples/webui/data/products/MANIFEST-000001 similarity index 100% rename from examples/all_types/data/sensors/MANIFEST-000001 rename to examples/webui/data/products/MANIFEST-000001 diff --git a/examples/webui/data/products/schema.json b/examples/webui/data/products/schema.json new file mode 100644 index 0000000..84ae916 --- /dev/null +++ b/examples/webui/data/products/schema.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/examples/webui/data/products/wal/CURRENT b/examples/webui/data/products/wal/CURRENT new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/webui/data/products/wal/CURRENT @@ -0,0 +1 @@ +1 diff --git a/examples/batch_insert/data/example2/CURRENT b/examples/webui/data/users/CURRENT similarity index 100% rename from examples/batch_insert/data/example2/CURRENT rename to examples/webui/data/users/CURRENT diff --git a/examples/new_types/data/api_logs/MANIFEST-000001 b/examples/webui/data/users/MANIFEST-000001 similarity index 100% rename from examples/new_types/data/api_logs/MANIFEST-000001 rename to examples/webui/data/users/MANIFEST-000001 diff --git a/examples/webui/data/users/schema.json b/examples/webui/data/users/schema.json new file mode 100644 index 0000000..f30f2f2 --- /dev/null +++ b/examples/webui/data/users/schema.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/examples/webui/data/users/wal/CURRENT b/examples/webui/data/users/wal/CURRENT new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/webui/data/users/wal/CURRENT @@ -0,0 +1 @@ +1 diff --git a/query.go b/query.go index 1af7840..668d3d3 100644 --- a/query.go +++ b/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 获取第一行 diff --git a/schema.go b/schema.go index 48132f2..7347d73 100644 --- a/schema.go +++ b/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) } diff --git a/sstable.go b/sstable.go index df4fd1e..3543090 100644 --- a/sstable.go +++ b/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) }