mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-26 16:40:17 +08:00
171 lines
3.9 KiB
TypeScript
171 lines
3.9 KiB
TypeScript
/**
|
|
* SQL 实验室查询模块
|
|
* 提供用户自定义 SQL 查询功能
|
|
*/
|
|
|
|
import { openDatabase } from '../core'
|
|
|
|
// 最大返回行数限制
|
|
const MAX_LIMIT = 1000
|
|
|
|
// 查询超时时间(毫秒)
|
|
const QUERY_TIMEOUT_MS = 10000
|
|
|
|
/**
|
|
* SQL 执行结果
|
|
*/
|
|
export interface SQLResult {
|
|
columns: string[]
|
|
rows: any[][]
|
|
rowCount: number
|
|
duration: number
|
|
limited: boolean // 是否被截断
|
|
}
|
|
|
|
/**
|
|
* 表结构信息
|
|
*/
|
|
export interface TableSchema {
|
|
name: string
|
|
columns: {
|
|
name: string
|
|
type: string
|
|
notnull: boolean
|
|
pk: boolean
|
|
}[]
|
|
}
|
|
|
|
/**
|
|
* 获取数据库 Schema
|
|
*/
|
|
export function getSchema(sessionId: string): TableSchema[] {
|
|
const db = openDatabase(sessionId)
|
|
if (!db) {
|
|
throw new Error('数据库不存在')
|
|
}
|
|
|
|
// 获取所有表名
|
|
const tables = db
|
|
.prepare(
|
|
`SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
ORDER BY name`
|
|
)
|
|
.all() as { name: string }[]
|
|
|
|
const schema: TableSchema[] = []
|
|
|
|
for (const table of tables) {
|
|
// 获取表的列信息
|
|
const columns = db.prepare(`PRAGMA table_info('${table.name}')`).all() as {
|
|
cid: number
|
|
name: string
|
|
type: string
|
|
notnull: number
|
|
dflt_value: any
|
|
pk: number
|
|
}[]
|
|
|
|
schema.push({
|
|
name: table.name,
|
|
columns: columns.map((col) => ({
|
|
name: col.name,
|
|
type: col.type,
|
|
notnull: col.notnull === 1,
|
|
pk: col.pk === 1,
|
|
})),
|
|
})
|
|
}
|
|
|
|
return schema
|
|
}
|
|
|
|
/**
|
|
* 解析并强制添加 LIMIT
|
|
* 如果 SQL 没有 LIMIT 或 LIMIT 超过最大值,强制设置为 MAX_LIMIT
|
|
*/
|
|
function enforceLimit(sql: string): { sql: string; limited: boolean } {
|
|
const trimmedSQL = sql.trim()
|
|
|
|
// 检查是否是 SELECT 语句
|
|
if (!trimmedSQL.toUpperCase().startsWith('SELECT')) {
|
|
return { sql: trimmedSQL, limited: false }
|
|
}
|
|
|
|
// 使用正则匹配 LIMIT 子句
|
|
const limitMatch = trimmedSQL.match(/\bLIMIT\s+(\d+)\s*(?:,\s*\d+)?(?:\s+OFFSET\s+\d+)?/i)
|
|
|
|
if (limitMatch) {
|
|
const currentLimit = parseInt(limitMatch[1], 10)
|
|
if (currentLimit > MAX_LIMIT) {
|
|
// 替换超出的 LIMIT
|
|
const newSQL = trimmedSQL.replace(/\bLIMIT\s+\d+/i, `LIMIT ${MAX_LIMIT}`)
|
|
return { sql: newSQL, limited: true }
|
|
}
|
|
return { sql: trimmedSQL, limited: false }
|
|
} else {
|
|
// 没有 LIMIT,追加
|
|
// 需要处理可能存在的分号
|
|
const sqlWithoutSemicolon = trimmedSQL.replace(/;\s*$/, '')
|
|
return { sql: `${sqlWithoutSemicolon} LIMIT ${MAX_LIMIT}`, limited: true }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 执行用户 SQL 查询
|
|
* - 只支持 SELECT 语句
|
|
* - 强制 LIMIT 不超过 MAX_LIMIT
|
|
* - 带超时控制
|
|
*/
|
|
export function executeRawSQL(sessionId: string, sql: string): SQLResult {
|
|
const db = openDatabase(sessionId)
|
|
if (!db) {
|
|
throw new Error('数据库不存在')
|
|
}
|
|
|
|
const trimmedSQL = sql.trim()
|
|
|
|
// 只允许 SELECT 语句
|
|
if (!trimmedSQL.toUpperCase().startsWith('SELECT')) {
|
|
throw new Error('只支持 SELECT 查询语句')
|
|
}
|
|
|
|
// 强制 LIMIT
|
|
const { sql: limitedSQL, limited } = enforceLimit(trimmedSQL)
|
|
|
|
// 执行查询
|
|
const startTime = Date.now()
|
|
|
|
try {
|
|
// better-sqlite3 是同步的,我们通过 Worker 实现"超时"
|
|
// 这里先执行,超时由 Worker 管理器控制
|
|
const stmt = db.prepare(limitedSQL)
|
|
const rows = stmt.all()
|
|
const duration = Date.now() - startTime
|
|
|
|
// 获取列名
|
|
const columns = stmt.columns().map((col) => col.name)
|
|
|
|
// 将结果转换为二维数组
|
|
const rowData = rows.map((row: any) => columns.map((col) => row[col]))
|
|
|
|
return {
|
|
columns,
|
|
rows: rowData,
|
|
rowCount: rows.length,
|
|
duration,
|
|
limited: limited || rows.length >= MAX_LIMIT,
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
// 美化错误信息
|
|
const message = error.message
|
|
.replace(/^SQLITE_ERROR: /, '')
|
|
.replace(/^SQLITE_READONLY: /, '只读模式:')
|
|
throw new Error(message)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|