sql-analyzer.js
7.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// SQL预执行分析器
import { executeExplain } from './db.js'
/**
* 对SQL进行预执行检查
* 1. 语法检查(通过EXPLAIN)
* 2. 执行计划分析(全表扫描、索引使用等)
* 3. 锁风险分析
* 4. 预估影响行数
*/
export async function analyzeSql(connId, sql) {
const result = {
syntaxOk: false,
syntaxError: null,
warnings: [],
risks: [],
explainRows: [],
estimatedRows: 0,
sqlType: detectSqlType(sql),
}
// 检测SQL类型,非SELECT给出风险提示
if (result.sqlType !== 'SELECT') {
result.risks.push({
level: 'high',
type: 'DML_OPERATION',
message: `当前SQL为 ${result.sqlType} 语句,可能造成数据变更和锁表风险`,
})
}
// 语法检查:通过EXPLAIN验证
try {
const explainRows = await executeExplain(connId, sql)
if (explainRows) {
result.syntaxOk = true
result.explainRows = explainRows
// 分析执行计划
for (const row of explainRows) {
const type = row.type || row.access_type
const key = row.key || row.key_name
const extra = row.Extra || row.extra || ''
const rows = row.rows || row.rows_examined || 0
const table = row.table_name || row.table || ''
// 累计预估行数
result.estimatedRows += Number(rows) || 0
// 检测全表扫描
if (type === 'ALL') {
result.warnings.push({
level: 'warn',
type: 'FULL_TABLE_SCAN',
message: `表 ${table} 将进行全表扫描,预估扫描 ${rows} 行`,
table,
})
}
// 检测未使用索引
if (!key || key === 'NULL' || key === null) {
result.warnings.push({
level: 'warn',
type: 'NO_INDEX',
message: `表 ${table} 未使用索引`,
table,
})
}
// 检测filesort
if (extra.includes('Using filesort')) {
result.warnings.push({
level: 'warn',
type: 'FILESORT',
message: `表 ${table} 使用了文件排序(filesort),大数据量下可能较慢`,
table,
})
}
// 检测临时表
if (extra.includes('Using temporary')) {
result.risks.push({
level: 'medium',
type: 'TEMPORARY_TABLE',
message: `表 ${table} 使用了临时表,可能影响性能`,
table,
})
}
}
// 大数据量风险
if (result.estimatedRows > 100000) {
result.risks.push({
level: 'high',
type: 'LARGE_RESULT',
message: `预估扫描行数 ${result.estimatedRows.toLocaleString()} 行,可能消耗大量资源`,
})
} else if (result.estimatedRows > 10000) {
result.warnings.push({
level: 'warn',
type: 'MEDIUM_RESULT',
message: `预估扫描行数 ${result.estimatedRows.toLocaleString()} 行,请关注执行时间`,
})
}
} else {
// EXPLAIN无法执行,但SQL可能是正确的(如某些DDL)
result.syntaxOk = true
result.warnings.push({
level: 'info',
type: 'NO_EXPLAIN',
message: '该SQL类型无法通过EXPLAIN分析,请自行评估风险',
})
}
} catch (e) {
result.syntaxOk = false
result.syntaxError = e.message
}
return result
}
/**
* 检测SQL类型
*/
function detectSqlType(sql) {
const normalized = normalizeSqlForDetection(sql)
if (/^\s*SELECT\b/i.test(normalized)) return 'SELECT'
if (/^\s*WITH\b/i.test(normalized)) return 'SELECT' // CTE + SELECT
if (/^\s*INSERT\b/i.test(normalized)) return 'INSERT'
if (/^\s*UPDATE\b/i.test(normalized)) return 'UPDATE'
if (/^\s*DELETE\b/i.test(normalized)) return 'DELETE'
if (/^\s*REPLACE\b/i.test(normalized)) return 'REPLACE'
if (/^\s*CREATE\b/i.test(normalized)) return 'CREATE'
if (/^\s*ALTER\b/i.test(normalized)) return 'ALTER'
if (/^\s*DROP\b/i.test(normalized)) return 'DROP'
if (/^\s*TRUNCATE\b/i.test(normalized)) return 'TRUNCATE'
if (/^\s*LOAD\s+DATA\b/i.test(normalized)) return 'LOAD_DATA'
if (/^\s*CALL\b/i.test(normalized)) return 'CALL'
if (/^\s*GRANT\b/i.test(normalized)) return 'GRANT'
if (/^\s*REVOKE\b/i.test(normalized)) return 'REVOKE'
return 'OTHER'
}
/**
* 去除SQL前缀注释和空白,用于类型检测
*/
function normalizeSqlForDetection(sql) {
// 去除单行注释 (-- 和 #)
let normalized = sql.replace(/--[^\n]*/g, ' ').replace(/#[^\n]*/g, ' ')
// 去除多行注释
normalized = normalized.replace(/\/\*[\s\S]*?\*\//g, ' ')
return normalized.trim()
}
/**
* 校验SQL是否为只读SELECT语句
* 支持检测多语句场景(分号分隔)
* @param {string} sql
* @returns {{ allowed: boolean, reason: string|null, detectedType: string }}
*/
export function validateSelectOnly(sql) {
if (!sql || !sql.trim()) {
return { allowed: false, reason: 'SQL内容不能为空', detectedType: 'EMPTY' }
}
// 分号分隔的多语句逐条检测
const statements = splitSqlStatements(sql)
for (let i = 0; i < statements.length; i++) {
const stmt = statements[i]
if (!stmt.trim()) continue
const type = detectSqlType(stmt)
if (type === 'SELECT') continue // SELECT允许
// 非SELECT语句,拒绝
const DANGEROUS_LABELS = {
'INSERT': 'INSERT(插入数据)',
'UPDATE': 'UPDATE(更新数据)',
'DELETE': 'DELETE(删除数据)',
'REPLACE': 'REPLACE(替换数据)',
'CREATE': 'CREATE(创建对象)',
'ALTER': 'ALTER(修改结构)',
'DROP': 'DROP(删除对象)',
'TRUNCATE': 'TRUNCATE(清空表)',
'LOAD_DATA': 'LOAD DATA(导入数据)',
'CALL': 'CALL(调用存储过程)',
'GRANT': 'GRANT(授权)',
'REVOKE': 'REVOKE(回收权限)',
'OTHER': '未知类型',
}
const label = DANGEROUS_LABELS[type] || type
return {
allowed: false,
reason: `脚本仅允许SELECT查询语句,检测到第${i + 1}条语句为 ${label},不允许执行`,
detectedType: type,
}
}
return { allowed: true, reason: null, detectedType: 'SELECT' }
}
/**
* 按分号分割SQL语句(忽略引号内的分号)
*/
function splitSqlStatements(sql) {
const statements = []
let current = ''
let inSingleQuote = false
let inDoubleQuote = false
for (let i = 0; i < sql.length; i++) {
const ch = sql[i]
const next = sql[i + 1]
if (ch === "'" && !inDoubleQuote) {
if (inSingleQuote && next === "'") {
// 转义单引号 ''
current += ch + next
i++
continue
}
inSingleQuote = !inSingleQuote
current += ch
continue
}
if (ch === '"' && !inSingleQuote) {
if (inDoubleQuote && next === '"') {
current += ch + next
i++
continue
}
inDoubleQuote = !inDoubleQuote
current += ch
continue
}
if (ch === ';' && !inSingleQuote && !inDoubleQuote) {
statements.push(current)
current = ''
continue
}
current += ch
}
if (current.trim()) {
statements.push(current)
}
return statements
}