@cmd233/mcp-database-server 1.1.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{readme.md → README.md} +10 -5
- package/dist/src/db/index.js +92 -0
- package/dist/src/db/mysql-adapter.js +13 -6
- package/dist/src/db/postgresql-adapter.js +14 -5
- package/dist/src/db/sql-validator.js +34 -0
- package/dist/src/db/sqlite-adapter.js +10 -3
- package/dist/src/db/sqlserver-adapter.js +126 -23
- package/dist/src/handlers/toolHandlers.js +527 -27
- package/dist/src/tools/insightTools.js +17 -5
- package/dist/src/tools/queryTools.js +14 -2
- package/dist/src/tools/schemaTools.js +342 -49
- package/package.json +2 -3
package/{readme.md → README.md}
RENAMED
|
@@ -105,6 +105,8 @@ node dist/src/index.js --mysql --host <host-name> --database <database-name> --p
|
|
|
105
105
|
必需参数:
|
|
106
106
|
- `--host`: MySQL 主机名或 IP 地址
|
|
107
107
|
- `--database`: 数据库名称
|
|
108
|
+
|
|
109
|
+
可选参数:
|
|
108
110
|
- `--port`: 端口号(默认: 3306)
|
|
109
111
|
|
|
110
112
|
可选参数:
|
|
@@ -285,15 +287,18 @@ MCP 数据库服务器提供以下可供 Claude 使用的工具:
|
|
|
285
287
|
| 工具 | 描述 | 必需参数 |
|
|
286
288
|
|------|-------------|---------------------|
|
|
287
289
|
| `read_query` | 执行 SELECT 查询以读取数据 | `query`: SQL SELECT 语句 |
|
|
288
|
-
| `write_query` | 执行 INSERT、UPDATE 或
|
|
289
|
-
| `create_table` | 在数据库中创建新表 | `query`: CREATE TABLE
|
|
290
|
-
| `alter_table` | 修改现有表架构 | `query`: ALTER TABLE
|
|
290
|
+
| `write_query` | 执行 INSERT、UPDATE、DELETE 或 TRUNCATE 查询 | `query`: SQL 修改语句<br>`confirm`: 安全标志(必须为 true) |
|
|
291
|
+
| `create_table` | 在数据库中创建新表 | `query`: CREATE TABLE 语句<br>`confirm`: 安全标志(必须为 true) |
|
|
292
|
+
| `alter_table` | 修改现有表架构 | `query`: ALTER TABLE 语句<br>`confirm`: 安全标志(必须为 true) |
|
|
291
293
|
| `drop_table` | 从数据库中删除表 | `table_name`: 表名<br>`confirm`: 安全标志(必须为 true) |
|
|
292
294
|
| `list_tables` | 获取所有表的列表 | 无 |
|
|
293
295
|
| `describe_table` | 查看表的架构信息 | `table_name`: 表名 |
|
|
294
296
|
| `export_query` | 将查询结果导出为 CSV/JSON | `query`: SQL SELECT 语句<br>`format`: "csv" 或 "json" |
|
|
295
|
-
| `append_insight` | 添加业务洞察到备忘录 | `insight`:
|
|
296
|
-
| `list_insights` | 列出所有业务洞察 | 无 |
|
|
297
|
+
| `append_insight` | 添加业务洞察到备忘录 (**仅 SQLite**) | `insight`: 洞察文本<br>`confirm`: 安全标志(必须为 true) |
|
|
298
|
+
| `list_insights` | 列出所有业务洞察 (**仅 SQLite**) | 无 |
|
|
299
|
+
| `list_views` | 列出所有视图 (**仅 SQL Server**) | 无 |
|
|
300
|
+
| `describe_view` | 获取视图结构 (**仅 SQL Server**) | `view_name`: 视图名 |
|
|
301
|
+
| `get_view_definition` | 获取视图定义 SQL (**仅 SQL Server**) | `view_name`: 视图名 |
|
|
297
302
|
|
|
298
303
|
有关如何在 Claude 中使用这些工具的实际示例,请参阅[使用示例](docs/usage-examples.md)。
|
|
299
304
|
|
package/dist/src/db/index.js
CHANGED
|
@@ -93,3 +93,95 @@ export function getDescribeTableQuery(tableName) {
|
|
|
93
93
|
}
|
|
94
94
|
return dbAdapter.getDescribeTableQuery(tableName);
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* 获取列出视图的数据库特定查询
|
|
98
|
+
* 仅 SQL Server 支持
|
|
99
|
+
* @returns SQL 查询字符串
|
|
100
|
+
* @throws 如果数据库不支持视图功能
|
|
101
|
+
*/
|
|
102
|
+
export function getListViewsQuery() {
|
|
103
|
+
if (!dbAdapter) {
|
|
104
|
+
throw new Error("数据库未初始化");
|
|
105
|
+
}
|
|
106
|
+
if (dbAdapter.getListViewsQuery) {
|
|
107
|
+
return dbAdapter.getListViewsQuery();
|
|
108
|
+
}
|
|
109
|
+
// 统一错误处理策略:不支持视图时抛出明确错误
|
|
110
|
+
throw new Error("当前数据库不支持视图功能");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 获取视图定义的数据库特定查询
|
|
114
|
+
* 仅 SQL Server 支持
|
|
115
|
+
* @param viewName 视图名
|
|
116
|
+
*/
|
|
117
|
+
export function getViewDefinitionQuery(viewName) {
|
|
118
|
+
if (!dbAdapter) {
|
|
119
|
+
throw new Error("数据库未初始化");
|
|
120
|
+
}
|
|
121
|
+
if (dbAdapter.getViewDefinitionQuery) {
|
|
122
|
+
return dbAdapter.getViewDefinitionQuery(viewName);
|
|
123
|
+
}
|
|
124
|
+
throw new Error("当前数据库不支持视图功能");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 检查数据库是否支持视图功能
|
|
128
|
+
*/
|
|
129
|
+
export function supportsViews() {
|
|
130
|
+
if (!dbAdapter) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return dbAdapter.supportsViews ? dbAdapter.supportsViews() : false;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 检查存储过程功能是否可用
|
|
137
|
+
* @returns 可用的数据库适配器
|
|
138
|
+
* @throws 如果数据库未初始化或不支持存储过程功能
|
|
139
|
+
*/
|
|
140
|
+
function requireProcedureSupport() {
|
|
141
|
+
if (!dbAdapter) {
|
|
142
|
+
throw new Error('数据库未初始化');
|
|
143
|
+
}
|
|
144
|
+
if (!dbAdapter.getListProceduresQuery) {
|
|
145
|
+
throw new Error('当前数据库不支持存储过程功能');
|
|
146
|
+
}
|
|
147
|
+
return dbAdapter;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 获取列出存储过程的查询
|
|
151
|
+
* 仅 SQL Server 支持
|
|
152
|
+
* @returns SQL 查询字符串
|
|
153
|
+
* @throws 如果数据库不支持存储过程功能
|
|
154
|
+
*/
|
|
155
|
+
export function getListProceduresQuery() {
|
|
156
|
+
return requireProcedureSupport().getListProceduresQuery();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 获取存储过程参数信息的查询
|
|
160
|
+
* 仅 SQL Server 支持
|
|
161
|
+
* @param procedureName 存储过程名
|
|
162
|
+
* @returns SQL 查询字符串
|
|
163
|
+
* @throws 如果数据库不支持存储过程功能
|
|
164
|
+
*/
|
|
165
|
+
export function getDescribeProcedureQuery(procedureName) {
|
|
166
|
+
return requireProcedureSupport().getDescribeProcedureQuery(procedureName);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 获取存储过程定义的查询
|
|
170
|
+
* 仅 SQL Server 支持
|
|
171
|
+
* @param procedureName 存储过程名
|
|
172
|
+
* @returns SQL 查询字符串
|
|
173
|
+
* @throws 如果数据库不支持存储过程功能
|
|
174
|
+
*/
|
|
175
|
+
export function getProcedureDefinitionQuery(procedureName) {
|
|
176
|
+
return requireProcedureSupport().getProcedureDefinitionQuery(procedureName);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 检查数据库是否支持存储过程功能
|
|
180
|
+
* @returns 如果支持返回 true,否则返回 false
|
|
181
|
+
*/
|
|
182
|
+
export function supportsProcedures() {
|
|
183
|
+
if (!dbAdapter) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return dbAdapter.supportsProcedures ? dbAdapter.supportsProcedures() : false;
|
|
187
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
1
2
|
import mysql from "mysql2/promise";
|
|
2
3
|
import { Signer } from "@aws-sdk/rds-signer";
|
|
3
4
|
/**
|
|
@@ -55,7 +56,7 @@ export class MysqlAdapter {
|
|
|
55
56
|
throw new Error("AWS IAM 认证需要 AWS 用户名参数");
|
|
56
57
|
}
|
|
57
58
|
try {
|
|
58
|
-
console.info(`[INFO]
|
|
59
|
+
console.info(`[INFO] 正在为区域 ${this.awsRegion} 生成 AWS 认证令牌, 主机: ${this.host}, 用户: ${this.config.user}`);
|
|
59
60
|
const signer = new Signer({
|
|
60
61
|
region: this.awsRegion,
|
|
61
62
|
hostname: this.host,
|
|
@@ -63,7 +64,7 @@ export class MysqlAdapter {
|
|
|
63
64
|
username: this.config.user,
|
|
64
65
|
});
|
|
65
66
|
const token = await signer.getAuthToken();
|
|
66
|
-
console.info(`[INFO] AWS
|
|
67
|
+
console.info(`[INFO] AWS 认证令牌生成成功`);
|
|
67
68
|
return token;
|
|
68
69
|
}
|
|
69
70
|
catch (err) {
|
|
@@ -76,10 +77,10 @@ export class MysqlAdapter {
|
|
|
76
77
|
*/
|
|
77
78
|
async init() {
|
|
78
79
|
try {
|
|
79
|
-
console.info(`[INFO]
|
|
80
|
+
console.info(`[INFO] 正在连接 MySQL: ${this.host}, 数据库: ${this.database}`);
|
|
80
81
|
// 处理 AWS IAM 认证
|
|
81
82
|
if (this.awsIamAuth) {
|
|
82
|
-
console.info(`[INFO]
|
|
83
|
+
console.info(`[INFO] 正在为用户 ${this.config.user} 使用 AWS IAM 认证`);
|
|
83
84
|
try {
|
|
84
85
|
const authToken = await this.generateAwsAuthToken();
|
|
85
86
|
// 使用生成的令牌作为密码创建新配置
|
|
@@ -97,10 +98,10 @@ export class MysqlAdapter {
|
|
|
97
98
|
else {
|
|
98
99
|
this.connection = await mysql.createConnection(this.config);
|
|
99
100
|
}
|
|
100
|
-
console.info(`[INFO] MySQL
|
|
101
|
+
console.info(`[INFO] MySQL 连接成功建立`);
|
|
101
102
|
}
|
|
102
103
|
catch (err) {
|
|
103
|
-
console.error(`[ERROR] MySQL
|
|
104
|
+
console.error(`[ERROR] MySQL 连接错误: ${err.message}`);
|
|
104
105
|
if (this.awsIamAuth) {
|
|
105
106
|
throw new Error(`使用 AWS IAM 认证连接 MySQL 失败: ${err.message}。请验证您的 AWS 凭据、IAM 权限和 RDS 配置。`);
|
|
106
107
|
}
|
|
@@ -116,6 +117,8 @@ export class MysqlAdapter {
|
|
|
116
117
|
if (!this.connection) {
|
|
117
118
|
throw new Error("数据库未初始化");
|
|
118
119
|
}
|
|
120
|
+
// 验证禁用的操作(防止恶意查询)
|
|
121
|
+
validateForbiddenOperations(query);
|
|
119
122
|
try {
|
|
120
123
|
const [rows] = await this.connection.execute(query, params);
|
|
121
124
|
return Array.isArray(rows) ? rows : [];
|
|
@@ -131,6 +134,8 @@ export class MysqlAdapter {
|
|
|
131
134
|
if (!this.connection) {
|
|
132
135
|
throw new Error("数据库未初始化");
|
|
133
136
|
}
|
|
137
|
+
// 验证禁用的操作
|
|
138
|
+
validateForbiddenOperations(query);
|
|
134
139
|
try {
|
|
135
140
|
const [result] = await this.connection.execute(query, params);
|
|
136
141
|
const changes = result.affectedRows || 0;
|
|
@@ -148,6 +153,8 @@ export class MysqlAdapter {
|
|
|
148
153
|
if (!this.connection) {
|
|
149
154
|
throw new Error("数据库未初始化");
|
|
150
155
|
}
|
|
156
|
+
// 验证禁用的操作
|
|
157
|
+
validateForbiddenOperations(query);
|
|
151
158
|
try {
|
|
152
159
|
await this.connection.query(query);
|
|
153
160
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
1
2
|
import pg from 'pg';
|
|
2
3
|
/**
|
|
3
4
|
* PostgreSQL 数据库适配器实现
|
|
@@ -24,7 +25,7 @@ export class PostgresqlAdapter {
|
|
|
24
25
|
*/
|
|
25
26
|
async init() {
|
|
26
27
|
try {
|
|
27
|
-
console.error(`[INFO]
|
|
28
|
+
console.error(`[INFO] 正在连接 PostgreSQL: ${this.host}, 数据库: ${this.database}`);
|
|
28
29
|
console.error(`[DEBUG] Connection details:`, {
|
|
29
30
|
host: this.host,
|
|
30
31
|
database: this.database,
|
|
@@ -35,10 +36,10 @@ export class PostgresqlAdapter {
|
|
|
35
36
|
});
|
|
36
37
|
this.client = new pg.Client(this.config);
|
|
37
38
|
await this.client.connect();
|
|
38
|
-
console.error(`[INFO] PostgreSQL
|
|
39
|
+
console.error(`[INFO] PostgreSQL 连接成功建立`);
|
|
39
40
|
}
|
|
40
41
|
catch (err) {
|
|
41
|
-
console.error(`[ERROR] PostgreSQL
|
|
42
|
+
console.error(`[ERROR] PostgreSQL 连接错误: ${err.message}`);
|
|
42
43
|
throw new Error(`连接 PostgreSQL 失败: ${err.message}`);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -52,9 +53,12 @@ export class PostgresqlAdapter {
|
|
|
52
53
|
if (!this.client) {
|
|
53
54
|
throw new Error("数据库未初始化");
|
|
54
55
|
}
|
|
56
|
+
// 验证禁用的操作(防止恶意查询)
|
|
57
|
+
validateForbiddenOperations(query);
|
|
55
58
|
try {
|
|
56
59
|
// PostgreSQL 使用 $1, $2 等作为参数化查询的占位符
|
|
57
|
-
|
|
60
|
+
let paramIndex = 0;
|
|
61
|
+
const preparedQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
|
|
58
62
|
const result = await this.client.query(preparedQuery, params);
|
|
59
63
|
return result.rows;
|
|
60
64
|
}
|
|
@@ -72,9 +76,12 @@ export class PostgresqlAdapter {
|
|
|
72
76
|
if (!this.client) {
|
|
73
77
|
throw new Error("数据库未初始化");
|
|
74
78
|
}
|
|
79
|
+
// 验证禁用的操作
|
|
80
|
+
validateForbiddenOperations(query);
|
|
75
81
|
try {
|
|
76
82
|
// 将 ? 替换为编号参数
|
|
77
|
-
|
|
83
|
+
let paramIndex = 0;
|
|
84
|
+
const preparedQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
|
|
78
85
|
let lastID = 0;
|
|
79
86
|
let changes = 0;
|
|
80
87
|
// 对于 INSERT 查询,尝试获取插入的 ID
|
|
@@ -106,6 +113,8 @@ export class PostgresqlAdapter {
|
|
|
106
113
|
if (!this.client) {
|
|
107
114
|
throw new Error("数据库未初始化");
|
|
108
115
|
}
|
|
116
|
+
// 验证禁用的操作
|
|
117
|
+
validateForbiddenOperations(query);
|
|
109
118
|
try {
|
|
110
119
|
await this.client.query(query);
|
|
111
120
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL 操作验证工具
|
|
3
|
+
* 集中管理禁用的 SQL 操作,确保数据库安全
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 禁用的 SQL 操作类型及其错误消息
|
|
7
|
+
* 使用更严格的模式,检测注释后的危险语句
|
|
8
|
+
*/
|
|
9
|
+
const FORBIDDEN_OPERATIONS = [
|
|
10
|
+
{
|
|
11
|
+
type: 'DROP',
|
|
12
|
+
// 匹配 DROP,忽略前导空白和单行注释
|
|
13
|
+
pattern: /^(\s*|--[^\n]*\n)*DROP\s/i,
|
|
14
|
+
message: 'DROP 操作已被禁用,此类操作应由 DBA 在数据库层面处理'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'TRUNCATE',
|
|
18
|
+
// 匹配 TRUNCATE,忽略前导空白和单行注释
|
|
19
|
+
pattern: /^(\s*|--[^\n]*\n)*TRUNCATE\s/i,
|
|
20
|
+
message: 'TRUNCATE 操作已被禁用,因为它不可回滚且不触发触发器'
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* 验证 SQL 查询是否包含禁用的操作
|
|
25
|
+
* @param query 要验证的 SQL 查询
|
|
26
|
+
* @throws Error 如果查询包含禁用的操作
|
|
27
|
+
*/
|
|
28
|
+
export function validateForbiddenOperations(query) {
|
|
29
|
+
for (const { pattern, message } of FORBIDDEN_OPERATIONS) {
|
|
30
|
+
if (pattern.test(query)) {
|
|
31
|
+
throw new Error(message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sqlite3 from "sqlite3";
|
|
2
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
2
3
|
/**
|
|
3
4
|
* SQLite 数据库适配器实现
|
|
4
5
|
*/
|
|
@@ -13,14 +14,14 @@ export class SqliteAdapter {
|
|
|
13
14
|
async init() {
|
|
14
15
|
return new Promise((resolve, reject) => {
|
|
15
16
|
// 确保数据库路径可访问
|
|
16
|
-
console.error(`[INFO]
|
|
17
|
+
console.error(`[INFO] 正在打开 SQLite 数据库: ${this.dbPath}`);
|
|
17
18
|
this.db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => {
|
|
18
19
|
if (err) {
|
|
19
|
-
console.error(`[ERROR] SQLite
|
|
20
|
+
console.error(`[ERROR] SQLite 连接错误: ${err.message}`);
|
|
20
21
|
reject(err);
|
|
21
22
|
}
|
|
22
23
|
else {
|
|
23
|
-
console.error("[INFO] SQLite
|
|
24
|
+
console.error("[INFO] SQLite 数据库成功打开");
|
|
24
25
|
resolve();
|
|
25
26
|
}
|
|
26
27
|
});
|
|
@@ -36,6 +37,8 @@ export class SqliteAdapter {
|
|
|
36
37
|
if (!this.db) {
|
|
37
38
|
throw new Error("数据库未初始化");
|
|
38
39
|
}
|
|
40
|
+
// 验证禁用的操作(防止恶意查询)
|
|
41
|
+
validateForbiddenOperations(query);
|
|
39
42
|
return new Promise((resolve, reject) => {
|
|
40
43
|
this.db.all(query, params, (err, rows) => {
|
|
41
44
|
if (err) {
|
|
@@ -57,6 +60,8 @@ export class SqliteAdapter {
|
|
|
57
60
|
if (!this.db) {
|
|
58
61
|
throw new Error("数据库未初始化");
|
|
59
62
|
}
|
|
63
|
+
// 验证禁用的操作
|
|
64
|
+
validateForbiddenOperations(query);
|
|
60
65
|
return new Promise((resolve, reject) => {
|
|
61
66
|
this.db.run(query, params, function (err) {
|
|
62
67
|
if (err) {
|
|
@@ -77,6 +82,8 @@ export class SqliteAdapter {
|
|
|
77
82
|
if (!this.db) {
|
|
78
83
|
throw new Error("数据库未初始化");
|
|
79
84
|
}
|
|
85
|
+
// 验证禁用的操作
|
|
86
|
+
validateForbiddenOperations(query);
|
|
80
87
|
return new Promise((resolve, reject) => {
|
|
81
88
|
this.db.exec(query, (err) => {
|
|
82
89
|
if (err) {
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
import { validateForbiddenOperations } from "./sql-validator.js";
|
|
1
2
|
import sql from 'mssql';
|
|
3
|
+
/**
|
|
4
|
+
* 验证并转义 SQL Server 标识符名称
|
|
5
|
+
* 防止 SQL 注入攻击
|
|
6
|
+
* @param name 标识符名称(表名、视图名、存储过程名等)
|
|
7
|
+
* @returns 转义后的安全标识符
|
|
8
|
+
* @throws 如果标识符包含非法字符
|
|
9
|
+
*/
|
|
10
|
+
function escapeIdentifier(name) {
|
|
11
|
+
// 检查名称是否为空
|
|
12
|
+
if (!name || typeof name !== 'string') {
|
|
13
|
+
throw new Error('标识符名称不能为空');
|
|
14
|
+
}
|
|
15
|
+
// 检查名称长度(SQL Server 限制为 128 字符)
|
|
16
|
+
if (name.length > 128) {
|
|
17
|
+
throw new Error('标识符名称长度超过限制(最大 128 字符)');
|
|
18
|
+
}
|
|
19
|
+
// 检查是否包含危险的 SQL 字符
|
|
20
|
+
// 只允许字母、数字、下划线和常见的安全字符
|
|
21
|
+
const validNamePattern = /^[a-zA-Z_][a-zA-Z0-9_@$#]*$/;
|
|
22
|
+
if (!validNamePattern.test(name)) {
|
|
23
|
+
throw new Error(`标识符名称包含非法字符: ${name.substring(0, 20)}...`);
|
|
24
|
+
}
|
|
25
|
+
// 使用方括号转义标识符
|
|
26
|
+
// 方括号内的右方括号需要转义为两个右方括号
|
|
27
|
+
return `[${name.replace(/]/g, ']]')}]`;
|
|
28
|
+
}
|
|
2
29
|
/**
|
|
3
30
|
* SQL Server 数据库适配器实现
|
|
4
31
|
*/
|
|
@@ -68,27 +95,27 @@ export class SqlServerAdapter {
|
|
|
68
95
|
await this.pool.close();
|
|
69
96
|
}
|
|
70
97
|
catch (closeErr) {
|
|
71
|
-
console.error(`[WARN]
|
|
98
|
+
console.error(`[WARN] 关闭旧连接池时出错: ${closeErr.message}`);
|
|
72
99
|
}
|
|
73
100
|
this.pool = null;
|
|
74
101
|
}
|
|
75
|
-
console.error(`[INFO]
|
|
102
|
+
console.error(`[INFO] 正在连接 SQL Server: ${this.server}, 数据库: ${this.database}`);
|
|
76
103
|
const pool = new sql.ConnectionPool(this.config);
|
|
77
104
|
// 使用单次监听器防止内存泄漏
|
|
78
105
|
pool.once('error', (err) => {
|
|
79
|
-
console.error(`[ERROR] SQL Server
|
|
106
|
+
console.error(`[ERROR] SQL Server 连接池错误: ${err.message}`);
|
|
80
107
|
// 标记连接池为不可用,下次查询时会自动重连
|
|
81
108
|
if (this.pool === pool) {
|
|
82
109
|
this.pool = null;
|
|
83
110
|
}
|
|
84
111
|
});
|
|
85
112
|
this.pool = await pool.connect();
|
|
86
|
-
console.error(`[INFO] SQL Server
|
|
113
|
+
console.error(`[INFO] SQL Server 连接成功建立`);
|
|
87
114
|
return this.pool;
|
|
88
115
|
}
|
|
89
116
|
catch (err) {
|
|
90
117
|
this.pool = null;
|
|
91
|
-
console.error(`[ERROR] SQL Server
|
|
118
|
+
console.error(`[ERROR] SQL Server 连接错误: ${err.message}`);
|
|
92
119
|
throw new Error(`连接 SQL Server 失败: ${err.message}`);
|
|
93
120
|
}
|
|
94
121
|
finally {
|
|
@@ -139,7 +166,7 @@ export class SqlServerAdapter {
|
|
|
139
166
|
// 只有在获取连接池后(即 poolAcquired = true)发生的连接错误才重试
|
|
140
167
|
// ensureConnection 本身的错误(如认证失败、连接超时)不应重试
|
|
141
168
|
if (isConnectionError && poolAcquired && attempt < retries && this.pool !== null) {
|
|
142
|
-
console.error(`[WARN]
|
|
169
|
+
console.error(`[WARN] 检测到连接错误,正在尝试重新连接 (尝试 ${attempt + 1}/${retries}): ${lastError.message}`);
|
|
143
170
|
this.pool = null;
|
|
144
171
|
poolAcquired = false; // 重置标记
|
|
145
172
|
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
|
|
@@ -163,7 +190,7 @@ export class SqlServerAdapter {
|
|
|
163
190
|
await this.pool.close();
|
|
164
191
|
}
|
|
165
192
|
catch (err) {
|
|
166
|
-
console.error(`[WARN]
|
|
193
|
+
console.error(`[WARN] 关闭连接池时出错: ${err.message}`);
|
|
167
194
|
}
|
|
168
195
|
this.pool = null;
|
|
169
196
|
}
|
|
@@ -176,6 +203,8 @@ export class SqlServerAdapter {
|
|
|
176
203
|
* @returns 包含查询结果的 Promise
|
|
177
204
|
*/
|
|
178
205
|
async all(query, params = []) {
|
|
206
|
+
// 验证禁用的操作(防止恶意查询)
|
|
207
|
+
validateForbiddenOperations(query);
|
|
179
208
|
return this.executeWithRetry(async (pool) => {
|
|
180
209
|
const request = pool.request();
|
|
181
210
|
// 向请求添加参数
|
|
@@ -196,6 +225,8 @@ export class SqlServerAdapter {
|
|
|
196
225
|
* @returns 包含结果信息的 Promise
|
|
197
226
|
*/
|
|
198
227
|
async run(query, params = []) {
|
|
228
|
+
// 验证禁用的操作
|
|
229
|
+
validateForbiddenOperations(query);
|
|
199
230
|
return this.executeWithRetry(async (pool) => {
|
|
200
231
|
const request = pool.request();
|
|
201
232
|
// 向请求添加参数
|
|
@@ -207,17 +238,22 @@ export class SqlServerAdapter {
|
|
|
207
238
|
const preparedQuery = query.replace(/\?/g, () => `@param${paramIndex++}`);
|
|
208
239
|
// 如果是 INSERT,添加标识值的输出参数
|
|
209
240
|
let lastID = 0;
|
|
241
|
+
let changes = 0;
|
|
210
242
|
if (query.trim().toUpperCase().startsWith('INSERT')) {
|
|
211
243
|
request.output('insertedId', sql.Int, 0);
|
|
212
244
|
const updatedQuery = `${preparedQuery}; SELECT @insertedId = SCOPE_IDENTITY();`;
|
|
213
245
|
const result = await request.query(updatedQuery);
|
|
214
246
|
lastID = result.output.insertedId || 0;
|
|
247
|
+
// 使用 rowsAffected 获取受影响行数
|
|
248
|
+
changes = result.rowsAffected?.[0] || (lastID > 0 ? 1 : 0);
|
|
215
249
|
}
|
|
216
250
|
else {
|
|
217
|
-
await request.query(preparedQuery);
|
|
251
|
+
const result = await request.query(preparedQuery);
|
|
252
|
+
// 使用 rowsAffected 获取受影响行数
|
|
253
|
+
changes = result.rowsAffected?.[0] || 0;
|
|
218
254
|
}
|
|
219
255
|
return {
|
|
220
|
-
changes:
|
|
256
|
+
changes: changes,
|
|
221
257
|
lastID: lastID
|
|
222
258
|
};
|
|
223
259
|
});
|
|
@@ -228,6 +264,8 @@ export class SqlServerAdapter {
|
|
|
228
264
|
* @returns 执行完成后解析的 Promise
|
|
229
265
|
*/
|
|
230
266
|
async exec(query) {
|
|
267
|
+
// 验证禁用的操作
|
|
268
|
+
validateForbiddenOperations(query);
|
|
231
269
|
return this.executeWithRetry(async (pool) => {
|
|
232
270
|
const request = pool.request();
|
|
233
271
|
await request.batch(query);
|
|
@@ -251,10 +289,13 @@ export class SqlServerAdapter {
|
|
|
251
289
|
return "SELECT TABLE_NAME as name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME";
|
|
252
290
|
}
|
|
253
291
|
/**
|
|
254
|
-
*
|
|
255
|
-
* @param tableName
|
|
292
|
+
* 获取描述表或视图的数据库特定查询
|
|
293
|
+
* @param tableName 表名或视图名
|
|
294
|
+
* @returns SQL 查询字符串
|
|
256
295
|
*/
|
|
257
296
|
getDescribeTableQuery(tableName) {
|
|
297
|
+
// 验证并转义表名,防止 SQL 注入
|
|
298
|
+
const escapedTableName = escapeIdentifier(tableName);
|
|
258
299
|
return `
|
|
259
300
|
SELECT
|
|
260
301
|
c.COLUMN_NAME as name,
|
|
@@ -272,27 +313,89 @@ export class SqlServerAdapter {
|
|
|
272
313
|
LEFT JOIN
|
|
273
314
|
sys.extended_properties ep
|
|
274
315
|
ON ep.major_id = (
|
|
275
|
-
SELECT
|
|
276
|
-
FROM sys.
|
|
277
|
-
INNER JOIN sys.schemas s ON
|
|
278
|
-
WHERE
|
|
316
|
+
SELECT o.object_id
|
|
317
|
+
FROM sys.objects o
|
|
318
|
+
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
|
|
319
|
+
WHERE o.name = ${escapedTableName} AND s.name = c.TABLE_SCHEMA
|
|
320
|
+
AND o.type IN ('U', 'V')
|
|
279
321
|
)
|
|
280
322
|
AND ep.minor_id = c.ORDINAL_POSITION
|
|
281
323
|
AND ep.name = 'MS_Description'
|
|
282
324
|
WHERE
|
|
283
|
-
c.TABLE_NAME =
|
|
325
|
+
c.TABLE_NAME = ${escapedTableName}
|
|
284
326
|
ORDER BY
|
|
285
327
|
c.ORDINAL_POSITION
|
|
286
328
|
`;
|
|
287
329
|
}
|
|
288
330
|
/**
|
|
289
|
-
*
|
|
331
|
+
* 获取列出视图的数据库特定查询
|
|
290
332
|
*/
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
333
|
+
getListViewsQuery() {
|
|
334
|
+
return "SELECT TABLE_NAME as name FROM INFORMATION_SCHEMA.VIEWS ORDER BY TABLE_NAME";
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 获取视图定义的数据库特定查询
|
|
338
|
+
* @param viewName 视图名
|
|
339
|
+
* @returns SQL 查询字符串
|
|
340
|
+
* 注意: 使用 WITH ENCRYPTION 创建的视图无法获取定义
|
|
341
|
+
*/
|
|
342
|
+
getViewDefinitionQuery(viewName) {
|
|
343
|
+
// 验证并转义视图名,防止 SQL 注入
|
|
344
|
+
const escapedViewName = escapeIdentifier(viewName);
|
|
345
|
+
return `SELECT VIEW_DEFINITION as definition FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = ${escapedViewName}`;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* 检查数据库是否支持视图功能
|
|
349
|
+
*/
|
|
350
|
+
supportsViews() {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* 检查数据库是否支持存储过程功能
|
|
355
|
+
*/
|
|
356
|
+
supportsProcedures() {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* 获取列出存储过程的数据库特定查询
|
|
361
|
+
*/
|
|
362
|
+
getListProceduresQuery() {
|
|
363
|
+
return "SELECT ROUTINE_NAME as name FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE' ORDER BY ROUTINE_NAME";
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* 获取存储过程参数信息的查询
|
|
367
|
+
* @param procedureName 存储过程名
|
|
368
|
+
* @returns SQL 查询字符串
|
|
369
|
+
*/
|
|
370
|
+
getDescribeProcedureQuery(procedureName) {
|
|
371
|
+
// 验证并转义存储过程名,防止 SQL 注入
|
|
372
|
+
const escapedProcedureName = escapeIdentifier(procedureName);
|
|
373
|
+
return `
|
|
374
|
+
SELECT
|
|
375
|
+
PARAMETER_NAME as name,
|
|
376
|
+
DATA_TYPE +
|
|
377
|
+
CASE
|
|
378
|
+
WHEN CHARACTER_MAXIMUM_LENGTH IS NOT NULL
|
|
379
|
+
THEN '(' + CAST(CHARACTER_MAXIMUM_LENGTH AS VARCHAR) + ')'
|
|
380
|
+
ELSE ''
|
|
381
|
+
END as type,
|
|
382
|
+
PARAMETER_MODE as direction,
|
|
383
|
+
CASE WHEN PARAMETER_MODE IN ('OUT', 'INOUT') THEN 1 ELSE 0 END as is_output,
|
|
384
|
+
NULL as default_value
|
|
385
|
+
FROM INFORMATION_SCHEMA.PARAMETERS
|
|
386
|
+
WHERE SPECIFIC_NAME = ${escapedProcedureName}
|
|
387
|
+
ORDER BY ORDINAL_POSITION
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 获取存储过程定义的查询
|
|
392
|
+
* @param procedureName 存储过程名
|
|
393
|
+
* @returns SQL 查询字符串
|
|
394
|
+
* 注意: 使用 WITH ENCRYPTION 创建的存储过程无法获取定义
|
|
395
|
+
*/
|
|
396
|
+
getProcedureDefinitionQuery(procedureName) {
|
|
397
|
+
// 验证并转义存储过程名,防止 SQL 注入
|
|
398
|
+
const escapedProcedureName = escapeIdentifier(procedureName);
|
|
399
|
+
return `SELECT ROUTINE_DEFINITION as definition FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME = ${escapedProcedureName}`;
|
|
297
400
|
}
|
|
298
401
|
}
|