最后更新: 2025-12-12
- ✅ 数据库列表(名称、类型、大小、表数量)
- ✅ 表列表(名称、行数)
- ✅ 表数据分页浏览
- ✅ 列排序(点击表头)
- ✅ Schema 查看
- ✅ 自定义 SQL 查询(仅 SELECT)
- ✅ 敏感数据库保护
- ✅ 状态持久化(切换 Tab 保持选中)
- ✅ 查询超时保护 (Phase 1.1 已完成)
- ✅ 结果集大小限制 (Phase 1.2 已完成)
- ✅ 并发查询限制 (Phase 1.3 已完成)
- ✅ Protobuf BLOB 解析 - 支持上传
.desc文件解析 BLOB 列 - ✅ 选中样式优化 - 实色主题块高亮
实现位置: DebugProbe/Sources/Database/SQLiteInspector.swift
配置参数:
/// SQLite busy_timeout(毫秒)- 等待数据库锁的最大时间
private let busyTimeout: Int32 = 5000 // 5 秒
/// 查询执行超时(秒)- 超时后强制中断查询
private let queryExecutionTimeout: TimeInterval = 10.0实现机制:
// 1. 设置超时定时器
var timedOut = false
let timeoutWorkItem = DispatchWorkItem {
timedOut = true
sqlite3_interrupt(db) // 强制中断查询
DebugLog.warning("[DBInspector] Query timeout after \(queryExecutionTimeout)s, interrupted")
}
DispatchQueue.global().asyncAfter(
deadline: .now() + queryExecutionTimeout,
execute: timeoutWorkItem
)
// 2. 在查询循环中检查超时状态
while !timedOut && rows.count < maxQueryRows {
let stepResult = sqlite3_step(stmt)
if timedOut { break }
if stepResult == SQLITE_INTERRUPT {
// 查询被中断(超时)
break
}
// ...
}
// 3. 超时后抛出错误
if timedOut {
throw DBInspectorError.timeout
}配置参数:
/// 单页最大行数(表数据浏览)
private let maxPageSize = 500
/// SQL 查询最大返回行数
private let maxQueryRows = 1000限制说明:
- 表数据分页浏览: 每页最多 500 行
- 自定义 SQL 查询: 最多返回 1000 行
- 超出限制会自动截断,不会报错
实现机制:
/// 串行队列确保线程安全
private let queue = DispatchQueue(label: "com.debug.dbinspector", qos: .userInitiated)
// 所有数据库操作都在串行队列中执行
return try await withCheckedThrowingContinuation { continuation in
queue.async {
// 查询操作...
}
}效果:
- 同一时间只能执行一个查询
- 多个查询请求会排队执行
- 防止并发查询导致的资源竞争
实现机制:
// 只允许 SELECT 语句
guard trimmedQuery.uppercased().hasPrefix("SELECT") else {
throw DBInspectorError.invalidQuery("Only SELECT statements are allowed")
}
// 禁止危险操作关键字
let dangerousPatterns = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "ATTACH", "DETACH"]
for pattern in dangerousPatterns {
if upperQuery.contains(pattern) {
throw DBInspectorError.invalidQuery("Query contains forbidden operation: \(pattern)")
}
}表数据筛选:
- 列筛选: WHERE column = 'value'
- 文本搜索: WHERE column LIKE '%keyword%'
- 范围筛选: WHERE column BETWEEN a AND b
- NULL 筛选: WHERE column IS NULL / IS NOT NULL
- 隐藏/显示列
- 列宽调整
- 列固定(freeze)
- 列重排序
自动识别并格式化:
- 时间戳 → 可读日期
- JSON 字符串 → 格式化展示
- Base64 → 解码预览
- Blob → 大小显示 + 下载
- URL → 可点击链接
- 双击行展开详情面板
- 显示完整字段值(不截断)
- JSON/XML 高亮显示
- 复制单个字段值
interface QueryHistory {
query: string
timestamp: Date
executionTimeMs: number
rowCount: number
dbId: string
}- 保存最近 50 条查询
- 支持搜索历史
- 一键重新执行
- 可视化 WHERE 条件构建
- 表/列自动补全
- JOIN 辅助
预设常用查询:
- SELECT COUNT(*) FROM {table}
- SELECT * FROM {table} ORDER BY {pk} DESC LIMIT 100
- PRAGMA table_info({table})
- PRAGMA index_list({table})
- 使用 CodeMirror 或 Monaco Editor
- 关键字高亮
- 错误提示
- 双击编辑
- UPDATE 语句生成
- 需要确认弹窗
- 新增行 (INSERT)
- 删除行 (DELETE)
- 复制行
- 导出选中行
// 在 DebugProbeSettings 中添加
var allowDatabaseWrite: Bool { get set }
var writeProtectedDatabases: [String] { get set }- 记录所有写操作
- 生成回滚 SQL
支持格式:
- CSV (默认)
- JSON
- SQL INSERT 语句
- Excel (xlsx)
- CSV 导入
- SQL 脚本执行(需安全审核)
- 一键导出整个数据库
- 下载 .sqlite 文件
- ER 图可视化
- 索引列表
- 触发器列表
- 外键关系
- EXPLAIN QUERY PLAN 可视化
- 慢查询标记
- 索引使用建议
- 两个表对比
- 两个数据库对比
- 差异高亮
- 测试数据生成器
- 支持 Faker 规则
| 阶段 | 功能 | 优先级 | 状态 | 预估工时 |
|---|---|---|---|---|
| 1.1 | 查询超时保护 | 🔴 Critical | ✅ 已完成 | - |
| 1.2 | 结果集大小限制 | 🔴 Critical | ✅ 已完成 | - |
| 1.3 | 并发查询限制 | 🔴 Critical | ✅ 已完成 | - |
| 1.4 | SQL 注入防护 | 🔴 Critical | ✅ 已完成 | - |
| 2.1 | 数据筛选 | 🟡 High | ⏳ 待开发 | 8h |
| 2.4 | 行详情视图 | 🟡 High | ⏳ 待开发 | 4h |
| 3.1 | 查询历史 | 🟡 High | ⏳ 待开发 | 4h |
| 2.3 | 数据格式化 | 🟡 High | ⏳ 待开发 | 6h |
| 3.4 | SQL 语法高亮 | 🟡 High | ⏳ 待开发 | 3h |
| 2.2 | 列操作 | 🟠 Medium | ⏳ 待开发 | 6h |
| 5.1 | CSV 导出 | 🟠 Medium | ⏳ 待开发 | 4h |
| 3.3 | 查询模板 | 🟠 Medium | ⏳ 待开发 | 2h |
位置: DebugProbe/Sources/Database/SQLiteInspector.swift
private func executeQueryInternal(at url: URL, dbId: String, query: String) throws -> DBQueryResponse {
let startTime = CFAbsoluteTimeGetCurrent()
let db = try openDatabase(at: url)
defer { sqlite3_close(db) }
// 设置超时定时器 - 超时后强制中断查询
var timedOut = false
let timeoutWorkItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
timedOut = true
sqlite3_interrupt(db)
DebugLog.warning("[DBInspector] Query timeout after \(self.queryExecutionTimeout)s, interrupted")
}
DispatchQueue.global().asyncAfter(
deadline: .now() + queryExecutionTimeout,
execute: timeoutWorkItem
)
defer { timeoutWorkItem.cancel() }
var stmt: OpaquePointer?
let prepareResult = sqlite3_prepare_v2(db, query, -1, &stmt, nil)
if timedOut {
sqlite3_finalize(stmt)
throw DBInspectorError.timeout
}
guard prepareResult == SQLITE_OK else {
let errorMessage = String(cString: sqlite3_errmsg(db))
throw DBInspectorError.invalidQuery(errorMessage)
}
defer { sqlite3_finalize(stmt) }
// ... 获取列信息 ...
// 执行查询并获取结果(限制最多 maxQueryRows 行)
var rows: [DBRow] = []
while !timedOut && rows.count < maxQueryRows {
let stepResult = sqlite3_step(stmt)
if timedOut { break }
if stepResult == SQLITE_DONE {
break
} else if stepResult == SQLITE_ROW {
// ... 读取行数据 ...
rows.append(DBRow(values: values))
} else if stepResult == SQLITE_INTERRUPT {
// 查询被中断(超时)
break
} else {
let errorMessage = String(cString: sqlite3_errmsg(db))
throw DBInspectorError.internalError("Query failed: \(errorMessage)")
}
}
// 检查是否因超时而中断
if timedOut {
throw DBInspectorError.timeout
}
// ... 返回结果 ...
}WebUI 存储方案:
// stores/dbStore.ts
interface QueryHistoryItem {
id: string
query: string
dbId: string
dbName: string
timestamp: number
executionTimeMs: number
rowCount: number
success: boolean
error?: string
}
// 使用 localStorage 持久化
const HISTORY_KEY = 'db_inspector_query_history'
const MAX_HISTORY = 50
// 在 executeQuery 成功/失败后添加记录
addToHistory: (item: Omit<QueryHistoryItem, 'id'>) => {
const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]')
const newItem = { ...item, id: crypto.randomUUID() }
history.unshift(newItem)
if (history.length > MAX_HISTORY) history.pop()
localStorage.setItem(HISTORY_KEY, JSON.stringify(history))
set({ queryHistory: history })
}