重构代码结构并添加完整功能

主要改动:
- 重构目录结构:合并子目录到根目录,简化项目结构
- 添加完整的查询 API:支持复杂条件查询、字段选择、游标模式
- 实现 LSM-Tree Compaction:7层结构、Score-based策略、后台异步合并
- 添加 Web UI:基于 Lit 的现代化管理界面,支持数据浏览和 Manifest 查看
- 完善文档:添加 README.md 和 examples/webui/README.md

新增功能:
- Query Builder:链式查询 API,支持 Eq/Lt/Gt/In/Between/Contains 等操作符
- Web UI 组件:srdb-app、srdb-table-list、srdb-data-view、srdb-manifest-view 等
- 列选择持久化:自动保存到 localStorage
- 刷新按钮:一键刷新当前视图
- 主题切换:深色/浅色主题支持

代码优化:
- 使用 Go 1.24 新特性:range 7、min()、maps.Copy()、slices.Sort()
- 统一组件命名:所有 Web Components 使用 srdb-* 前缀
- CSS 优化:提取共享样式,减少重复代码
- 清理遗留代码:删除未使用的方法和样式
This commit is contained in:
2025-10-08 16:42:31 +08:00
parent ae87c38776
commit 23843493b8
64 changed files with 8374 additions and 6396 deletions

View File

@@ -1,254 +1,413 @@
# SRDB Web UI Example
# SRDB WebUI - 数据库管理工具
这个示例展示了如何使用 SRDB 的内置 Web UI 来可视化查看数据库中的表和数据
一个功能强大的 SRDB 数据库管理工具,集成了现代化的 Web 界面和实用的命令行工具
## 功能特性
## 📋 目录
- 📊 **表列表展示** - 左侧显示所有表及其行数
- 🔍 **Schema 查看** - 点击箭头展开查看表的字段定义
- 📋 **数据分页浏览** - 右侧以表格形式展示数据,支持分页
- 🎨 **响应式设计** - 现代化的界面设计
- **零构建** - 使用 HTMX 从 CDN 加载,无需构建步骤
- 💾 **大数据优化** - 自动截断显示,悬停查看,点击弹窗查看完整内容
- 📏 **数据大小显示** - 超过 1KB 的单元格自动显示大小标签
- 🔄 **后台数据插入** - 自动生成 2KB~512KB 的测试数据(每秒一条)
- [功能特性](#功能特性)
- [快速开始](#快速开始)
- [Web UI 使用指南](#web-ui-使用指南)
- [命令行工具](#命令行工具)
- [技术架构](#技术架构)
- [开发说明](#开发说明)
## 运行示例
---
## 🎯 功能特性
### Web UI
#### 📊 数据管理
- **表列表** - 查看所有表及其 Schema 信息
- **数据浏览** - 分页浏览表数据,支持自定义列选择
- **列持久化** - 自动保存列选择偏好到 localStorage
- **数据详情** - 点击查看完整的行数据JSON 格式)
- **智能截断** - 长字符串自动截断,点击查看完整内容
- **时间格式化** - 自动格式化 `_time` 字段为可读时间
#### 🌳 LSM-Tree 管理
- **Manifest 视图** - 可视化 LSM-Tree 层级结构
- **文件详情** - 查看每层的 SST 文件信息
- **Compaction 监控** - 实时查看 Compaction Score 和统计
- **层级折叠** - 可展开/收起查看文件详情
#### 🎨 用户体验
- **响应式设计** - 完美适配桌面和移动设备
- **深色/浅色主题** - 支持主题切换
- **实时刷新** - 一键刷新当前视图数据
- **移动端优化** - 侧边栏抽屉式导航
### 命令行工具
提供多个实用的数据库诊断和管理工具:
| 命令 | 功能 | 说明 |
|------|------|------|
| `serve` | Web UI 服务器 | 启动 Web 管理界面 |
| `check-data` | 数据检查 | 检查表数据完整性 |
| `check-seq` | 序列号检查 | 验证特定序列号的数据 |
| `dump-manifest` | Manifest 导出 | 导出 LSM-Tree 结构信息 |
| `inspect-sst` | SST 文件检查 | 检查单个 SST 文件 |
| `inspect-all-sst` | 批量 SST 检查 | 检查所有 SST 文件 |
| `test-fix` | 修复测试 | 测试数据修复功能 |
| `test-keys` | 键测试 | 测试键的存在性 |
---
## 🚀 快速开始
### 1. 启动 Web UI
```bash
# 进入示例目录
cd examples/webui
# 运行
go run main.go
# 使用默认配置(数据库:./data端口8080
go run main.go serve
# 自定义配置
go run main.go serve --db /path/to/database --port 3000
# 启用自动数据插入(用于演示)
go run main.go serve --auto-insert
```
程序会:
1. 创建/打开数据库目录 `./data`
2. 创建三个示例表:`users``products``logs`
3. 插入初始示例数据
4. **启动后台协程** - 每秒向 `logs` 表插入一条 2KB~512KB 的随机数据
5. 启动 Web 服务器在 `http://localhost:8080`
### 2. 访问 Web UI
## 使用界面
打开浏览器访问http://localhost:8080
打开浏览器访问 `http://localhost:8080`,你将看到:
### 左侧边栏
- 显示所有表的列表
- 显示每个表的字段数量
- 点击 ▶ 图标展开查看字段信息
- 点击表名选择要查看的表(蓝色高亮显示当前选中)
### 右侧主区域
- **Schema 区域**:显示表结构和字段定义
- **Data 区域**:以表格形式显示数据
- 支持分页浏览(每页 20 条)
- 显示系统字段_seq, _time和用户字段
- **自动截断长数据**:超过 400px 的内容显示省略号
- **鼠标悬停**:悬停在单元格上查看完整内容
- **点击查看**:点击单元格在弹窗中查看完整内容
- **大小指示**:超过 1KB 的数据显示大小标签
### 大数据查看
1. **表格截断**:单元格最大宽度 400px超长显示 `...`
2. **悬停展开**:鼠标悬停自动展开,黄色背景高亮
3. **模态框**:点击单元格弹出窗口
- 等宽字体显示(适合查看十六进制数据)
- 显示数据大小
- 支持滚动查看超长内容
## API 端点
Web UI 提供了以下 HTTP API
### 获取所有表
```
GET /api/tables
```
返回示例:
```json
[
{
"name": "users",
"rowCount": 5,
"dir": "./data/users"
}
]
```
### 获取表的 Schema
```
GET /api/tables/{name}/schema
```
返回示例:
```json
{
"fields": [
{"name": "name", "type": "string", "required": true},
{"name": "email", "type": "string", "required": true},
{"name": "age", "type": "int", "required": false}
]
}
```
### 获取表数据(分页)
```
GET /api/tables/{name}/data?page=1&pageSize=20
```
参数:
- `page` - 页码,从 1 开始默认1
- `pageSize` - 每页行数,最大 100默认20
返回示例:
```json
{
"page": 1,
"pageSize": 20,
"totalRows": 5,
"totalPages": 1,
"rows": [
{
"_seq": 1,
"_time": 1234567890,
"name": "Alice",
"email": "alice@example.com",
"age": 30
}
]
}
```
### 获取表基本信息
```
GET /api/tables/{name}
```
## 在你的应用中使用
你可以在自己的应用中轻松集成 Web UI
```go
package main
import (
"net/http"
"code.tczkiot.com/srdb"
)
func main() {
// 打开数据库
db, _ := database.Open("./mydb")
defer db.Close()
// 获取 HTTP Handler
handler := db.WebUI()
// 启动服务器
http.ListenAndServe(":8080", handler)
}
```
或者将其作为现有 Web 应用的一部分:
```go
mux := http.NewServeMux()
// 你的其他路由
mux.HandleFunc("/api/myapp", myHandler)
// 挂载 SRDB Web UI 到 /admin/db 路径
mux.Handle("/admin/db/", http.StripPrefix("/admin/db", db.WebUI()))
http.ListenAndServe(":8080", mux)
```
## 技术栈
- **后端**: Go + 标准库 `net/http`
- **前端**: [HTMX](https://htmx.org/) + 原生 JavaScript + CSS
- **渲染**: 服务端 HTML 渲染Go 模板生成)
- **字体**: Google Fonts (Inter)
- **无构建**: 直接从 CDN 加载 HTMX无需 npm、webpack 等工具
- **部署**: 所有静态资源通过 `embed.FS` 嵌入到二进制文件中
## 测试大数据
### logs 表自动生成
程序会在后台持续向 `logs` 表插入大数据:
- **频率**:每秒一条
- **大小**2KB ~ 512KB 随机
- **格式**:十六进制字符串
- **字段**
- `timestamp` - 插入时间
- `data` - 随机数据(十六进制)
- `size_bytes` - 数据大小(字节)
你可以选择 `logs` 表来测试大数据的显示效果:
1. 单元格会显示数据大小标签(如 `245.12 KB`
2. 内容被自动截断,显示省略号
3. 点击单元格在弹窗中查看完整数据
终端会实时输出插入日志:
```
Inserted record #1, size: 245.12 KB
Inserted record #2, size: 128.50 KB
Inserted record #3, size: 487.23 KB
```
## 注意事项
- Web UI 是只读的,不提供数据修改功能
- 适合用于开发、调试和数据查看
- 生产环境建议添加身份验证和访问控制
- 大数据量表的分页查询性能取决于数据分布
- `logs` 表会持续增长,可手动删除 `./data/logs` 目录重置
## Compaction 状态
由于后台持续插入大数据,会产生大量 SST 文件。SRDB 会自动运行 compaction 合并这些文件。
### 检查 Compaction 状态
### 3. 命令行工具示例
```bash
# 查看 SST 文件分布
./check_sst.sh
# 检查表数据
go run main.go check-data --db ./data --table users
# 观察 webui 日志中的 [Compaction] 信息
# 检查特定序列号
go run main.go check-seq --db ./data --table users --seq 123
# 导出 Manifest
go run main.go dump-manifest --db ./data --table users
# 检查 SST 文件
go run main.go inspect-sst --db ./data --table users --file 000001.sst
```
### Compaction 改进
---
- **触发阈值**: L0 文件数量 ≥2 就触发(之前是 4
- **运行频率**: 每 10 秒自动检查
- **日志增强**: 显示详细的 compaction 状态和统计
## 📖 Web UI 使用指南
详细说明请查看 [COMPACTION.md](./COMPACTION.md)
### 界面布局
## 常见问题
### `invalid header` 错误
如果看到类似错误:
```
failed to open table logs: invalid header
┌─────────────────────────────────────────────────┐
│ SRDB Tables [🌙/☀️] │ ← 侧边栏
│ ├─ users │
│ ├─ orders │
│ └─ logs │
├─────────────────────────────────────────────────┤
│ [☰] users [🔄 Refresh] │ ← 页头
│ [Data] [Manifest / LSM-Tree] │ ← 视图切换
├─────────────────────────────────────────────────┤
│ │
│ Schema (点击字段卡片选择要显示的列) │ ← Schema 区域
│ ┌──────────┬──────────┬──────────┐ │
│ │⚡ id │● name │⚡ email │ │
│ │[int64] │[string] │[string] │ │
│ └──────────┴──────────┴──────────┘ │
│ │
│ Data (1,234 rows) │ ← 数据表格
│ ┌─────┬──────┬───────────┬─────────┐ │
│ │ _seq│ name │ email │ Actions │ │
│ ├─────┼──────┼───────────┼─────────┤ │
│ │ 1 │ John │ john@... │ Detail │ │
│ │ 2 │ Jane │ jane@... │ Detail │ │
│ └─────┴──────┴───────────┴─────────┘ │
│ │
├─────────────────────────────────────────────────┤
│ [10/page] [Previous] Page 1 of 5 [Go] [Next] │ ← 分页控件
└─────────────────────────────────────────────────┘
```
**快速修复**
### 功能说明
#### 1. 表列表(侧边栏)
- 显示所有表及其字段信息
- 点击表名切换到该表
- 展开/收起查看字段详情
- 字段图标:⚡ = 已索引,● = 未索引
#### 2. Data 视图
- **Schema 区域**:点击字段卡片选择要显示的列
- **数据表格**:显示选中的列数据
- **系统字段**
- `_seq`:序列号(第一列)
- `_time`:时间戳(倒数第二列,自动格式化)
- **Detail 按钮**查看完整的行数据JSON 格式)
- **分页控件**
- 每页大小10/20/50/100
- 上一页/下一页
- 跳转到指定页
#### 3. Manifest 视图
- **统计卡片**
- Active Levels活跃层数
- Total Files总文件数
- Total Size总大小
- CompactionsCompaction 次数
- **层级卡片**
- 点击展开/收起查看文件列表
- Score 指示器:
- 🟢 绿色:健康(< 50%
- 🟡 黄色警告50-80%
- 🔴 红色需要 Compaction(≥ 80%
- **文件详情**
- 文件编号大小行数
- Seq 范围min_key - max_key
#### 4. 刷新按钮
- 点击刷新当前视图的数据
- Data 视图重新加载表数据
- Manifest 视图重新加载 LSM-Tree 结构
#### 5. 主题切换
- 点击右上角的 🌙/☀ 图标
- 切换深色/浅色主题
- 自动保存到 localStorage
---
## 🛠️ 命令行工具
### serve - Web UI 服务器
启动 Web 管理界面
```bash
./fix_corrupted_table.sh logs
go run main.go serve [flags]
```
详见:[QUICK_FIX.md](./QUICK_FIX.md) 或 [TROUBLESHOOTING.md](./TROUBLESHOOTING.md)
**参数**
- `--db` - 数据库目录默认`./data`
- `--port` - 服务端口默认`8080`
- `--auto-insert` - 启用自动数据插入用于演示
## 更多信息
**示例**
```bash
# 基本使用
go run main.go serve
- [FEATURES.md](./FEATURES.md) - 详细功能说明
- [COMPACTION.md](./COMPACTION.md) - Compaction 机制和诊断
- [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) - 故障排除指南
- [QUICK_FIX.md](./QUICK_FIX.md) - 快速修复常见错误
# 自定义端口
go run main.go serve --port 3000
# 启用自动插入(每秒插入一条随机数据到 logs 表)
go run main.go serve --auto-insert
```
### check-data - 数据检查
检查表数据的完整性
```bash
go run main.go check-data --db ./data --table <table_name>
```
### check-seq - 序列号检查
验证特定序列号的数据
```bash
go run main.go check-seq --db ./data --table <table_name> --seq <sequence_number>
```
### dump-manifest - Manifest 导出
导出 LSM-Tree 层级结构信息
```bash
go run main.go dump-manifest --db ./data --table <table_name>
```
### inspect-sst - SST 文件检查
检查单个 SST 文件的内容和元数据
```bash
go run main.go inspect-sst --db ./data --table <table_name> --file <file_name>
```
### inspect-all-sst - 批量 SST 检查
检查表的所有 SST 文件
```bash
go run main.go inspect-all-sst --db ./data --table <table_name>
```
---
## 🏗️ 技术架构
### 前端技术栈
- **Lit** - 轻量级 Web Components 框架
- **ES Modules** - 原生 JavaScript 模块
- **CSS Variables** - 主题系统
- **Shadow DOM** - 组件封装
### 组件架构
```
srdb-app (主应用)
├── srdb-theme-toggle (主题切换)
├── srdb-table-list (表列表)
├── srdb-page-header (页头)
│ └── [🔄 Refresh] (刷新按钮)
├── srdb-table-view (表视图容器)
│ ├── srdb-data-view (数据视图)
│ │ ├── Schema 区域
│ │ │ ├── srdb-field-icon (字段图标)
│ │ │ └── srdb-badge (类型标签)
│ │ └── 数据表格
│ └── srdb-manifest-view (Manifest 视图)
│ └── srdb-badge (Score 标签)
└── srdb-modal-dialog (模态对话框)
```
### 后端架构
```
webui.go
├── API Endpoints
│ ├── GET /api/tables - 获取表列表
│ ├── GET /api/tables/{name}/schema - 获取表 Schema
│ ├── GET /api/tables/{name}/data - 获取表数据(分页)
│ ├── GET /api/tables/{name}/data/{seq} - 获取单条数据
│ └── GET /api/tables/{name}/manifest - 获取 Manifest 信息
├── Static Files
│ └── /static/* - 静态资源服务
└── Index
└── / - 首页
```
### 数据流
```
用户操作
组件事件 (CustomEvent)
app.js (事件总线)
API 请求 (fetch)
webui.go (HTTP Handler)
SRDB Database
JSON 响应
组件更新 (Lit reactive)
UI 渲染
```
---
## 🔧 开发说明
### 项目结构
```
webui/
├── commands/
│ └── webui.go # Web UI 服务器实现
├── static/
│ ├── index.html # 主页面
│ ├── css/
│ │ └── styles.css # 全局样式
│ └── js/
│ ├── app.js # 应用入口和事件总线
│ ├── components/ # Web Components
│ │ ├── app.js # 主应用容器
│ │ ├── badge.js # 标签组件
│ │ ├── data-view.js # 数据视图
│ │ ├── field-icon.js # 字段图标
│ │ ├── manifest-view.js # Manifest 视图
│ │ ├── modal-dialog.js # 模态对话框
│ │ ├── page-header.js # 页头
│ │ ├── table-list.js # 表列表
│ │ ├── table-view.js # 表视图容器
│ │ └── theme-toggle.js # 主题切换
│ └── styles/
│ └── shared-styles.js # 共享样式
└── webui.go # Web UI 后端实现
```
### 添加新组件
1. `static/js/components/` 创建组件文件
2. 继承 `LitElement`
3. 定义 `static properties` `static styles`
4. 实现 `render()` 方法
5. 使用 `customElements.define('srdb-xxx', Component)` 注册
6. `app.js` 中导入
### 添加新 API
1. `webui.go` 中添加 handler 方法
2. `setupHandler()` 中注册路由
3. 返回 JSON 格式的响应
4. 在前端组件中调用 API
### 样式规范
- 使用 CSS Variables 定义颜色和尺寸
- 组件样式封装在 Shadow DOM
- 共享样式定义在 `shared-styles.js`
- 响应式断点768px
### 命名规范
- **组件名**`srdb-xxx`kebab-case
- **类名**`ComponentName`PascalCase
- **文件名**`component-name.js`kebab-case
- **CSS **`.class-name`kebab-case
---
## 📝 注意事项
### 性能优化
1. **列选择**只加载选中的列减少数据传输
2. **字符串截断**长字符串自动截断按需加载完整内容
3. **分页加载**大表数据分页加载避免一次性加载全部
4. **Shadow DOM**组件样式隔离避免全局样式污染
### 浏览器兼容性
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
需要支持
- ES Modules
- Web Components
- Shadow DOM
- CSS Variables
### 已知限制
1. **大数据量**单页最多显示 1000 条数据
2. **字符串长度**超过 100 字符自动截断
3. **并发限制**同时只能查看一个表的数据
---
## 🤝 贡献
欢迎提交 Issue Pull Request
## 📄 许可证
MIT License

View File

@@ -9,7 +9,7 @@ import (
"strconv"
"strings"
"code.tczkiot.com/srdb/sst"
"code.tczkiot.com/srdb"
)
// InspectAllSST 检查所有 SST 文件
@@ -35,7 +35,7 @@ func InspectAllSST(sstDir string) {
for _, filename := range sstFiles {
sstPath := filepath.Join(sstDir, filename)
reader, err := sst.NewReader(sstPath)
reader, err := srdb.NewSSTableReader(sstPath)
if err != nil {
fmt.Printf("%s: ERROR - %v\n", filename, err)
continue

View File

@@ -5,7 +5,7 @@ import (
"log"
"os"
"code.tczkiot.com/srdb/sst"
"code.tczkiot.com/srdb"
)
// InspectSST 检查特定 SST 文件
@@ -19,7 +19,7 @@ func InspectSST(sstPath string) {
fmt.Printf("Size: %d bytes\n", info.Size())
// Open reader
reader, err := sst.NewReader(sstPath)
reader, err := srdb.NewSSTableReader(sstPath)
if err != nil {
log.Fatal(err)
}

View File

@@ -24,14 +24,14 @@ func StartWebUI(dbPath string, addr string) {
// 创建示例 Schema
userSchema := srdb.NewSchema("users", []srdb.Field{
{Name: "name", Type: srdb.FieldTypeString, Indexed: false, Comment: "User name"},
{Name: "name", Type: srdb.FieldTypeString, Indexed: true, Comment: "User name"},
{Name: "email", Type: srdb.FieldTypeString, Indexed: false, Comment: "Email address"},
{Name: "age", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "Age"},
{Name: "city", Type: srdb.FieldTypeString, Indexed: false, Comment: "City"},
})
productSchema := srdb.NewSchema("products", []srdb.Field{
{Name: "product_name", Type: srdb.FieldTypeString, Indexed: false, Comment: "Product name"},
{Name: "product_name", Type: srdb.FieldTypeString, Indexed: true, Comment: "Product name"},
{Name: "price", Type: srdb.FieldTypeFloat, Indexed: false, Comment: "Price"},
{Name: "quantity", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "Quantity"},
{Name: "category", Type: srdb.FieldTypeString, Indexed: false, Comment: "Category"},
@@ -56,7 +56,7 @@ func StartWebUI(dbPath string, addr string) {
log.Printf("Create users table failed: %v", err)
} else {
// 插入一些示例数据
users := []map[string]interface{}{
users := []map[string]any{
{"name": "Alice", "email": "alice@example.com", "age": 30, "city": "Beijing"},
{"name": "Bob", "email": "bob@example.com", "age": 25, "city": "Shanghai"},
{"name": "Charlie", "email": "charlie@example.com", "age": 35, "city": "Guangzhou"},
@@ -76,7 +76,7 @@ func StartWebUI(dbPath string, addr string) {
log.Printf("Create products table failed: %v", err)
} else {
// 插入一些示例数据
products := []map[string]interface{}{
products := []map[string]any{
{"product_name": "Laptop", "price": 999.99, "quantity": 10, "category": "Electronics"},
{"product_name": "Mouse", "price": 29.99, "quantity": 50, "category": "Electronics"},
{"product_name": "Keyboard", "price": 79.99, "quantity": 30, "category": "Electronics"},
@@ -126,15 +126,15 @@ func autoInsertData(db *srdb.Database) {
defer ticker.Stop()
counter := 1
var logsTable *srdb.Table
for range ticker.C {
tables := db.ListTables()
var logsTable *srdb.Table
hasLogs := slices.Contains(tables, "logs")
if !hasLogs {
logsSchema := srdb.NewSchema("logs", []srdb.Field{
{Name: "group", Type: srdb.FieldTypeString, Indexed: true, Comment: "Log group (A-E)"},
{Name: "timestamp", Type: srdb.FieldTypeString, Indexed: false, Comment: "Timestamp"},
{Name: "data", Type: srdb.FieldTypeString, Indexed: false, Comment: "Random data"},
{Name: "size_bytes", Type: srdb.FieldTypeInt64, Indexed: false, Comment: "Data size in bytes"},
@@ -151,7 +151,6 @@ func autoInsertData(db *srdb.Database) {
var err error
logsTable, err = db.GetTable("logs")
if err != nil || logsTable == nil {
log.Printf("Failed to get logs table: %v", err)
continue
}
}
@@ -159,7 +158,12 @@ func autoInsertData(db *srdb.Database) {
data := generateRandomData()
sizeBytes := len(data)
// 随机选择一个组 (A-E)
groups := []string{"A", "B", "C", "D", "E"}
group := groups[counter%len(groups)]
record := map[string]any{
"group": group,
"timestamp": time.Now().Format(time.RFC3339),
"data": data,
"size_bytes": int64(sizeBytes),
@@ -170,7 +174,7 @@ func autoInsertData(db *srdb.Database) {
log.Printf("Failed to insert data: %v", err)
} else {
sizeStr := formatBytes(sizeBytes)
log.Printf("Inserted record #%d, size: %s", counter, sizeStr)
log.Printf("Inserted record #%d, group: %s, size: %s", counter, group, sizeStr)
counter++
}
}