universal-db-mcp 2.10.1 → 2.12.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 +3 -0
- package/README.zh-CN.md +3 -0
- package/dist/adapters/dm.d.ts +11 -0
- package/dist/adapters/dm.d.ts.map +1 -1
- package/dist/adapters/dm.js +223 -131
- package/dist/adapters/dm.js.map +1 -1
- package/dist/adapters/gaussdb.d.ts +13 -1
- package/dist/adapters/gaussdb.d.ts.map +1 -1
- package/dist/adapters/gaussdb.js +126 -75
- package/dist/adapters/gaussdb.js.map +1 -1
- package/dist/adapters/goldendb.d.ts +14 -1
- package/dist/adapters/goldendb.d.ts.map +1 -1
- package/dist/adapters/goldendb.js +114 -80
- package/dist/adapters/goldendb.js.map +1 -1
- package/dist/adapters/highgo.d.ts +19 -1
- package/dist/adapters/highgo.d.ts.map +1 -1
- package/dist/adapters/highgo.js +139 -74
- package/dist/adapters/highgo.js.map +1 -1
- package/dist/adapters/kingbase.d.ts +18 -1
- package/dist/adapters/kingbase.d.ts.map +1 -1
- package/dist/adapters/kingbase.js +204 -146
- package/dist/adapters/kingbase.js.map +1 -1
- package/dist/adapters/mysql.d.ts +14 -1
- package/dist/adapters/mysql.d.ts.map +1 -1
- package/dist/adapters/mysql.js +116 -81
- package/dist/adapters/mysql.js.map +1 -1
- package/dist/adapters/oceanbase.d.ts +14 -1
- package/dist/adapters/oceanbase.d.ts.map +1 -1
- package/dist/adapters/oceanbase.js +114 -80
- package/dist/adapters/oceanbase.js.map +1 -1
- package/dist/adapters/oracle.d.ts +7 -1
- package/dist/adapters/oracle.d.ts.map +1 -1
- package/dist/adapters/oracle.js +178 -118
- package/dist/adapters/oracle.js.map +1 -1
- package/dist/adapters/polardb.d.ts +14 -1
- package/dist/adapters/polardb.d.ts.map +1 -1
- package/dist/adapters/polardb.js +68 -34
- package/dist/adapters/polardb.js.map +1 -1
- package/dist/adapters/postgres.d.ts +19 -1
- package/dist/adapters/postgres.d.ts.map +1 -1
- package/dist/adapters/postgres.js +205 -150
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/adapters/sqlserver.d.ts +1 -0
- package/dist/adapters/sqlserver.d.ts.map +1 -1
- package/dist/adapters/sqlserver.js +54 -35
- package/dist/adapters/sqlserver.js.map +1 -1
- package/dist/adapters/tidb.d.ts +14 -1
- package/dist/adapters/tidb.d.ts.map +1 -1
- package/dist/adapters/tidb.js +68 -34
- package/dist/adapters/tidb.js.map +1 -1
- package/dist/adapters/vastbase.d.ts +19 -1
- package/dist/adapters/vastbase.d.ts.map +1 -1
- package/dist/adapters/vastbase.js +122 -57
- package/dist/adapters/vastbase.js.map +1 -1
- package/dist/core/database-service.d.ts +6 -1
- package/dist/core/database-service.d.ts.map +1 -1
- package/dist/core/database-service.js +37 -3
- package/dist/core/database-service.js.map +1 -1
- package/dist/http/routes/schema.js +2 -2
- package/dist/http/routes/schema.js.map +1 -1
- package/dist/mcp/mcp-server.js +4 -4
- package/dist/mcp/mcp-server.js.map +1 -1
- package/dist/types/adapter.d.ts +3 -1
- package/dist/types/adapter.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,9 @@ AI: Let me query that for you...
|
|
|
59
59
|
- **Intelligent Caching** - Schema caching with configurable TTL for blazing-fast performance
|
|
60
60
|
- **Batch Query Optimization** - Up to 100x faster schema retrieval for large databases
|
|
61
61
|
- **Schema Enhancement** - Table comments, implicit relationship inference for better Text2SQL accuracy
|
|
62
|
+
- **Multi-Schema Support** - Automatic discovery of all user schemas (PostgreSQL, SQL Server, Oracle, DM, and more)
|
|
62
63
|
- **Data Masking** - Automatic sensitive data protection (phone, email, ID card, bank card, etc.)
|
|
64
|
+
- **Connection Stability** - Connection pooling, TCP Keep-Alive, and automatic reconnection for long-running sessions
|
|
63
65
|
|
|
64
66
|
### Performance Improvements
|
|
65
67
|
|
|
@@ -236,6 +238,7 @@ See [Dify Integration Guide](./docs/integrations/DIFY.md) for detailed setup ins
|
|
|
236
238
|
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
|
237
239
|
│ │ Database Adapter Layer │ │
|
|
238
240
|
│ │ MySQL │ PostgreSQL │ Redis │ Oracle │ MongoDB │ SQLite │ ... │ │
|
|
241
|
+
│ │ (Connection Pool + TCP Keep-Alive + Auto-Retry) │ │
|
|
239
242
|
│ └──────────────────────────────────────────────────────────────────┘ │
|
|
240
243
|
│ │
|
|
241
244
|
└─────────────────────────────────────────────────────────────────────────┘
|
package/README.zh-CN.md
CHANGED
|
@@ -58,7 +58,9 @@ AI: 让我帮你查询一下...
|
|
|
58
58
|
- **智能缓存** - Schema 缓存支持可配置的 TTL,性能极速
|
|
59
59
|
- **批量查询优化** - 大型数据库的 Schema 获取速度提升高达 100 倍
|
|
60
60
|
- **Schema 增强** - 表注释、隐式关系推断,提升 Text2SQL 准确性
|
|
61
|
+
- **多 Schema 支持** - 自动发现所有用户 Schema(PostgreSQL、SQL Server、Oracle、达梦等)
|
|
61
62
|
- **数据脱敏** - 自动保护敏感数据(手机号、邮箱、身份证、银行卡等)
|
|
63
|
+
- **连接稳定性** - 连接池、TCP Keep-Alive、断线自动重试,保障长时间会话稳定运行
|
|
62
64
|
|
|
63
65
|
### 性能提升
|
|
64
66
|
|
|
@@ -235,6 +237,7 @@ POST http://localhost:3000/mcp
|
|
|
235
237
|
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
|
236
238
|
│ │ 数据库适配器层 │ │
|
|
237
239
|
│ │ MySQL │ PostgreSQL │ Redis │ Oracle │ MongoDB │ SQLite │ ... │ │
|
|
240
|
+
│ │ (连接池 + TCP Keep-Alive + 断线自动重试) │ │
|
|
238
241
|
│ └──────────────────────────────────────────────────────────────────┘ │
|
|
239
242
|
│ │
|
|
240
243
|
└─────────────────────────────────────────────────────────────────────────┘
|
package/dist/adapters/dm.d.ts
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* dmdb 驱动会作为可选依赖自动安装
|
|
6
6
|
*
|
|
7
7
|
* 性能优化:使用批量查询获取 Schema 信息,避免 N+1 查询问题
|
|
8
|
+
*
|
|
9
|
+
* 连接管理:使用心跳保活 + 断线自动重连 + 操作自动重试,确保长连接稳定性
|
|
8
10
|
*/
|
|
9
11
|
import type { DbAdapter, QueryResult, SchemaInfo } from '../types/adapter.js';
|
|
10
12
|
export declare class DMAdapter implements DbAdapter {
|
|
11
13
|
private connection;
|
|
14
|
+
private heartbeatTimer;
|
|
15
|
+
private connectionConfig;
|
|
12
16
|
private config;
|
|
13
17
|
constructor(config: {
|
|
14
18
|
host: string;
|
|
@@ -17,6 +21,11 @@ export declare class DMAdapter implements DbAdapter {
|
|
|
17
21
|
password?: string;
|
|
18
22
|
database?: string;
|
|
19
23
|
});
|
|
24
|
+
private isConnectionError;
|
|
25
|
+
private reconnect;
|
|
26
|
+
private startHeartbeat;
|
|
27
|
+
private stopHeartbeat;
|
|
28
|
+
private withRetry;
|
|
20
29
|
/**
|
|
21
30
|
* 连接到达梦数据库
|
|
22
31
|
*/
|
|
@@ -41,10 +50,12 @@ export declare class DMAdapter implements DbAdapter {
|
|
|
41
50
|
* 因此需要按索引位置访问数据。
|
|
42
51
|
*/
|
|
43
52
|
getSchema(): Promise<SchemaInfo>;
|
|
53
|
+
private _getSchemaImpl;
|
|
44
54
|
/**
|
|
45
55
|
* 按索引获取对象的值(dmdb 驱动返回的列名是数字索引)
|
|
46
56
|
*/
|
|
47
57
|
private getValueByIndex;
|
|
58
|
+
private makeTableKey;
|
|
48
59
|
/**
|
|
49
60
|
* 组装 Schema 信息(处理 dmdb 驱动返回的数字索引列名)
|
|
50
61
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dm.d.ts","sourceRoot":"","sources":["../../src/adapters/dm.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"dm.d.ts","sourceRoot":"","sources":["../../src/adapters/dm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EACX,UAAU,EAMX,MAAM,qBAAqB,CAAC;AAyB7B,qBAAa,SAAU,YAAW,SAAS;IACzC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,MAAM,CAMZ;gBAEU,MAAM,EAAE;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB;IAID,OAAO,CAAC,iBAAiB;YAKX,SAAS;IAWvB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,aAAa;YAIP,SAAS;IAOvB;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAqC9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;OAEG;IACG,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IAkE3E;;;;;;;;;;OAUG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC;YAcxB,cAAc;IAkI5B;;OAEG;IACH,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACH,OAAO,CAAC,6BAA6B;IAyRrC;;OAEG;IACH,OAAO,CAAC,YAAY;IAuEpB;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;CA+BzC"}
|
package/dist/adapters/dm.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* dmdb 驱动会作为可选依赖自动安装
|
|
6
6
|
*
|
|
7
7
|
* 性能优化:使用批量查询获取 Schema 信息,避免 N+1 查询问题
|
|
8
|
+
*
|
|
9
|
+
* 连接管理:使用心跳保活 + 断线自动重连 + 操作自动重试,确保长连接稳定性
|
|
8
10
|
*/
|
|
9
11
|
import { isWriteOperation as checkWriteOperation } from '../utils/safety.js';
|
|
10
12
|
// 动态导入 dmdb,因为它是可选依赖
|
|
@@ -27,10 +29,64 @@ async function loadDMDB() {
|
|
|
27
29
|
}
|
|
28
30
|
export class DMAdapter {
|
|
29
31
|
connection = null;
|
|
32
|
+
heartbeatTimer = null;
|
|
33
|
+
connectionConfig = null;
|
|
30
34
|
config;
|
|
31
35
|
constructor(config) {
|
|
32
36
|
this.config = config;
|
|
33
37
|
}
|
|
38
|
+
isConnectionError(error) {
|
|
39
|
+
const msg = String(error?.message || '');
|
|
40
|
+
return /closed|ECONNRESET|EPIPE|ETIMEDOUT|ECONNREFUSED|网络|连接/.test(msg);
|
|
41
|
+
}
|
|
42
|
+
async reconnect() {
|
|
43
|
+
try {
|
|
44
|
+
if (this.connection) {
|
|
45
|
+
try {
|
|
46
|
+
await this.connection.close();
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
this.connection = null;
|
|
50
|
+
}
|
|
51
|
+
const DM = await loadDMDB();
|
|
52
|
+
this.connection = await DM.getConnection(this.connectionConfig);
|
|
53
|
+
console.error('达梦数据库重连成功');
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('达梦数据库重连失败:', error instanceof Error ? error.message : String(error));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
startHeartbeat() {
|
|
60
|
+
this.stopHeartbeat();
|
|
61
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
62
|
+
try {
|
|
63
|
+
if (this.connection) {
|
|
64
|
+
await this.connection.execute('SELECT 1 FROM DUAL', []);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
await this.reconnect();
|
|
69
|
+
}
|
|
70
|
+
}, 30000);
|
|
71
|
+
}
|
|
72
|
+
stopHeartbeat() {
|
|
73
|
+
if (this.heartbeatTimer) {
|
|
74
|
+
clearInterval(this.heartbeatTimer);
|
|
75
|
+
this.heartbeatTimer = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async withRetry(fn) {
|
|
79
|
+
try {
|
|
80
|
+
return await fn();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (this.isConnectionError(error)) {
|
|
84
|
+
await this.reconnect();
|
|
85
|
+
return await fn();
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
34
90
|
/**
|
|
35
91
|
* 连接到达梦数据库
|
|
36
92
|
*/
|
|
@@ -50,8 +106,10 @@ export class DMAdapter {
|
|
|
50
106
|
loginEncrypt: false,
|
|
51
107
|
};
|
|
52
108
|
this.connection = await DM.getConnection(connectionConfig);
|
|
109
|
+
this.connectionConfig = connectionConfig;
|
|
53
110
|
// 测试连接
|
|
54
111
|
await this.connection.execute('SELECT 1 FROM DUAL', []);
|
|
112
|
+
this.startHeartbeat();
|
|
55
113
|
}
|
|
56
114
|
catch (error) {
|
|
57
115
|
// 翻译常见错误
|
|
@@ -69,6 +127,7 @@ export class DMAdapter {
|
|
|
69
127
|
* 断开数据库连接
|
|
70
128
|
*/
|
|
71
129
|
async disconnect() {
|
|
130
|
+
this.stopHeartbeat();
|
|
72
131
|
if (this.connection) {
|
|
73
132
|
try {
|
|
74
133
|
await this.connection.close();
|
|
@@ -94,9 +153,9 @@ export class DMAdapter {
|
|
|
94
153
|
cleanQuery = cleanQuery.slice(0, -1).trim();
|
|
95
154
|
}
|
|
96
155
|
// 执行查询
|
|
97
|
-
const result = await this.connection.execute(cleanQuery, params || [], {
|
|
156
|
+
const result = await this.withRetry(() => this.connection.execute(cleanQuery, params || [], {
|
|
98
157
|
autoCommit: false,
|
|
99
|
-
});
|
|
158
|
+
}));
|
|
100
159
|
const executionTime = Date.now() - startTime;
|
|
101
160
|
// 处理查询结果
|
|
102
161
|
if (result.rows && result.rows.length > 0) {
|
|
@@ -159,85 +218,97 @@ export class DMAdapter {
|
|
|
159
218
|
throw new Error('数据库未连接');
|
|
160
219
|
}
|
|
161
220
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
221
|
+
return await this.withRetry(() => this._getSchemaImpl());
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
throw new Error(`获取数据库结构失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async _getSchemaImpl() {
|
|
228
|
+
// 获取达梦数据库版本
|
|
229
|
+
const versionResult = await this.connection.execute(`SELECT BANNER FROM V$VERSION WHERE ROWNUM = 1`, []);
|
|
230
|
+
const version = versionResult.rows?.[0]
|
|
231
|
+
? this.getValueByIndex(versionResult.rows[0], 0)
|
|
232
|
+
: 'unknown';
|
|
233
|
+
// 获取当前 schema(在达梦中,schema 通常与用户名相同)
|
|
234
|
+
const schemaResult = await this.connection.execute(`SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL`, []);
|
|
235
|
+
const currentSchema = schemaResult.rows?.[0]
|
|
236
|
+
? this.getValueByIndex(schemaResult.rows[0], 0)
|
|
237
|
+
: '';
|
|
238
|
+
// 获取当前用户(作为备选)
|
|
239
|
+
const userResult = await this.connection.execute('SELECT USER FROM DUAL', []);
|
|
240
|
+
const currentUser = userResult.rows?.[0]
|
|
241
|
+
? this.getValueByIndex(userResult.rows[0], 0)
|
|
242
|
+
: '';
|
|
243
|
+
// schema 名称:优先使用当前 schema,其次使用当前用户,最后使用配置的用户名
|
|
244
|
+
const schemaName = (currentSchema || currentUser || this.config.user || '').toUpperCase();
|
|
245
|
+
const databaseName = schemaName || 'unknown';
|
|
246
|
+
// 获取所有表的列信息
|
|
247
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=COLUMN_NAME, 3=DATA_TYPE, 4=DATA_LENGTH,
|
|
248
|
+
// 5=DATA_PRECISION, 6=DATA_SCALE, 7=NULLABLE, 8=DATA_DEFAULT, 9=COLUMN_ID
|
|
249
|
+
const allColumnsResult = await this.connection.execute(`SELECT OWNER, TABLE_NAME, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION,
|
|
184
250
|
DATA_SCALE, NULLABLE, DATA_DEFAULT, COLUMN_ID
|
|
185
|
-
FROM
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
251
|
+
FROM ALL_TAB_COLUMNS
|
|
252
|
+
WHERE OWNER NOT IN ('SYS', 'SYSTEM', 'SYSAUDITOR', 'SYSSSO', 'SYSDBA', 'CTISYS')
|
|
253
|
+
ORDER BY OWNER, TABLE_NAME, COLUMN_ID`, []);
|
|
254
|
+
// 获取所有列注释
|
|
255
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=COLUMN_NAME, 3=COMMENTS
|
|
256
|
+
const allCommentsResult = await this.connection.execute(`SELECT OWNER, TABLE_NAME, COLUMN_NAME, COMMENTS
|
|
257
|
+
FROM ALL_COL_COMMENTS
|
|
258
|
+
WHERE OWNER NOT IN ('SYS', 'SYSTEM', 'SYSAUDITOR', 'SYSSSO', 'SYSDBA', 'CTISYS')
|
|
259
|
+
AND COMMENTS IS NOT NULL`, []);
|
|
260
|
+
// 获取所有主键信息
|
|
261
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=COLUMN_NAME, 3=POSITION
|
|
262
|
+
const allPrimaryKeysResult = await this.connection.execute(`SELECT cons.OWNER, cons.TABLE_NAME, cols.COLUMN_NAME, cols.POSITION
|
|
263
|
+
FROM ALL_CONSTRAINTS cons
|
|
264
|
+
JOIN ALL_CONS_COLUMNS cols
|
|
265
|
+
ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME AND cons.OWNER = cols.OWNER
|
|
198
266
|
WHERE cons.CONSTRAINT_TYPE = 'P'
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
267
|
+
AND cons.OWNER NOT IN ('SYS', 'SYSTEM', 'SYSAUDITOR', 'SYSSSO', 'SYSDBA', 'CTISYS')
|
|
268
|
+
ORDER BY cons.OWNER, cons.TABLE_NAME, cols.POSITION`, []);
|
|
269
|
+
// 获取所有索引信息
|
|
270
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=INDEX_NAME, 3=UNIQUENESS, 4=COLUMN_NAME, 5=COLUMN_POSITION
|
|
271
|
+
const allIndexesResult = await this.connection.execute(`SELECT i.OWNER, i.TABLE_NAME, i.INDEX_NAME, i.UNIQUENESS, ic.COLUMN_NAME, ic.COLUMN_POSITION
|
|
272
|
+
FROM ALL_INDEXES i
|
|
273
|
+
JOIN ALL_IND_COLUMNS ic
|
|
274
|
+
ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER
|
|
275
|
+
WHERE i.OWNER NOT IN ('SYS', 'SYSTEM', 'SYSAUDITOR', 'SYSSSO', 'SYSDBA', 'CTISYS')
|
|
276
|
+
ORDER BY i.OWNER, i.TABLE_NAME, i.INDEX_NAME, ic.COLUMN_POSITION`, []);
|
|
277
|
+
// 获取所有表的行数估算和表注释
|
|
278
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=NUM_ROWS, 3=TABLE_COMMENT
|
|
279
|
+
const allStatsResult = await this.connection.execute(`SELECT t.OWNER, t.TABLE_NAME, t.NUM_ROWS, c.COMMENTS AS TABLE_COMMENT
|
|
280
|
+
FROM ALL_TABLES t
|
|
281
|
+
LEFT JOIN ALL_TAB_COMMENTS c ON t.TABLE_NAME = c.TABLE_NAME AND t.OWNER = c.OWNER
|
|
282
|
+
WHERE t.OWNER NOT IN ('SYS', 'SYSTEM', 'SYSAUDITOR', 'SYSSSO', 'SYSDBA', 'CTISYS')`, []);
|
|
283
|
+
// 获取所有外键信息
|
|
284
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=CONSTRAINT_NAME, 3=COLUMN_NAME, 4=REFERENCED_TABLE,
|
|
285
|
+
// 5=REFERENCED_COLUMN, 6=DELETE_RULE, 7=REF_OWNER, 8=POSITION
|
|
286
|
+
let allForeignKeys = [];
|
|
287
|
+
try {
|
|
288
|
+
const allForeignKeysResult = await this.connection.execute(`SELECT
|
|
289
|
+
c.OWNER,
|
|
217
290
|
c.TABLE_NAME,
|
|
218
291
|
c.CONSTRAINT_NAME,
|
|
219
292
|
cc.COLUMN_NAME,
|
|
220
293
|
rc.TABLE_NAME AS REFERENCED_TABLE,
|
|
221
294
|
rcc.COLUMN_NAME AS REFERENCED_COLUMN,
|
|
222
295
|
c.DELETE_RULE,
|
|
296
|
+
rc.OWNER AS REF_OWNER,
|
|
223
297
|
cc.POSITION
|
|
224
|
-
FROM
|
|
225
|
-
JOIN
|
|
226
|
-
JOIN
|
|
227
|
-
JOIN
|
|
298
|
+
FROM ALL_CONSTRAINTS c
|
|
299
|
+
JOIN ALL_CONS_COLUMNS cc ON c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME AND c.OWNER = cc.OWNER
|
|
300
|
+
JOIN ALL_CONSTRAINTS rc ON c.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND c.R_OWNER = rc.OWNER
|
|
301
|
+
JOIN ALL_CONS_COLUMNS rcc ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME AND rc.OWNER = rcc.OWNER AND cc.POSITION = rcc.POSITION
|
|
228
302
|
WHERE c.CONSTRAINT_TYPE = 'R'
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
catch (error) {
|
|
233
|
-
// 外键查询失败时忽略,返回空数组
|
|
234
|
-
console.error('获取外键信息失败,跳过:', error instanceof Error ? error.message : String(error));
|
|
235
|
-
}
|
|
236
|
-
return this.assembleSchemaFromIndexedRows(databaseName, version, allColumnsResult.rows || [], allCommentsResult.rows || [], allPrimaryKeysResult.rows || [], allIndexesResult.rows || [], allStatsResult.rows || [], allForeignKeys);
|
|
303
|
+
AND c.OWNER NOT IN ('SYS', 'SYSTEM', 'SYSAUDITOR', 'SYSSSO', 'SYSDBA', 'CTISYS')
|
|
304
|
+
ORDER BY c.OWNER, c.TABLE_NAME, c.CONSTRAINT_NAME, cc.POSITION`, []);
|
|
305
|
+
allForeignKeys = allForeignKeysResult.rows || [];
|
|
237
306
|
}
|
|
238
307
|
catch (error) {
|
|
239
|
-
|
|
308
|
+
// 外键查询失败时忽略,返回空数组
|
|
309
|
+
console.error('获取外键信息失败,跳过:', error instanceof Error ? error.message : String(error));
|
|
240
310
|
}
|
|
311
|
+
return this.assembleSchemaFromIndexedRows(databaseName, version, allColumnsResult.rows || [], allCommentsResult.rows || [], allPrimaryKeysResult.rows || [], allIndexesResult.rows || [], allStatsResult.rows || [], allForeignKeys, schemaName);
|
|
241
312
|
}
|
|
242
313
|
/**
|
|
243
314
|
* 按索引获取对象的值(dmdb 驱动返回的列名是数字索引)
|
|
@@ -258,31 +329,38 @@ export class DMAdapter {
|
|
|
258
329
|
const values = Object.values(obj);
|
|
259
330
|
return values.length > index ? values[index] : undefined;
|
|
260
331
|
}
|
|
332
|
+
makeTableKey(owner, tableName, currentUser) {
|
|
333
|
+
return owner.toUpperCase() === currentUser.toUpperCase() ? tableName : `${owner}.${tableName}`;
|
|
334
|
+
}
|
|
261
335
|
/**
|
|
262
336
|
* 组装 Schema 信息(处理 dmdb 驱动返回的数字索引列名)
|
|
263
337
|
*/
|
|
264
|
-
assembleSchemaFromIndexedRows(databaseName, version, allColumns, allComments, allPrimaryKeys, allIndexes, allStats, allForeignKeys) {
|
|
338
|
+
assembleSchemaFromIndexedRows(databaseName, version, allColumns, allComments, allPrimaryKeys, allIndexes, allStats, allForeignKeys, currentUser) {
|
|
265
339
|
// 按表名分组列信息
|
|
266
|
-
// 列顺序: 0=
|
|
267
|
-
//
|
|
340
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=COLUMN_NAME, 3=DATA_TYPE, 4=DATA_LENGTH,
|
|
341
|
+
// 5=DATA_PRECISION, 6=DATA_SCALE, 7=NULLABLE, 8=DATA_DEFAULT, 9=COLUMN_ID
|
|
268
342
|
const columnsByTable = new Map();
|
|
343
|
+
const schemaByTable = new Map();
|
|
269
344
|
for (const col of allColumns) {
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
const
|
|
345
|
+
const owner = this.getValueByIndex(col, 0);
|
|
346
|
+
const tableName = this.getValueByIndex(col, 1);
|
|
347
|
+
const columnName = this.getValueByIndex(col, 2);
|
|
348
|
+
const dataType = this.getValueByIndex(col, 3);
|
|
349
|
+
const dataLength = this.getValueByIndex(col, 4);
|
|
350
|
+
const dataPrecision = this.getValueByIndex(col, 5);
|
|
351
|
+
const dataScale = this.getValueByIndex(col, 6);
|
|
352
|
+
const nullable = this.getValueByIndex(col, 7);
|
|
353
|
+
const dataDefault = this.getValueByIndex(col, 8);
|
|
278
354
|
// 跳过无效数据
|
|
279
355
|
if (!tableName || !columnName) {
|
|
280
356
|
continue;
|
|
281
357
|
}
|
|
282
|
-
|
|
283
|
-
|
|
358
|
+
const tableKey = this.makeTableKey(owner, tableName, currentUser);
|
|
359
|
+
if (!columnsByTable.has(tableKey)) {
|
|
360
|
+
columnsByTable.set(tableKey, []);
|
|
361
|
+
schemaByTable.set(tableKey, owner);
|
|
284
362
|
}
|
|
285
|
-
columnsByTable.get(
|
|
363
|
+
columnsByTable.get(tableKey).push({
|
|
286
364
|
name: String(columnName).toLowerCase(),
|
|
287
365
|
type: this.formatDMType(dataType, dataLength, dataPrecision, dataScale),
|
|
288
366
|
nullable: nullable === 'Y',
|
|
@@ -290,24 +368,26 @@ export class DMAdapter {
|
|
|
290
368
|
});
|
|
291
369
|
}
|
|
292
370
|
// 按表名分组列注释
|
|
293
|
-
// 列顺序: 0=
|
|
371
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=COLUMN_NAME, 3=COMMENTS
|
|
294
372
|
const commentsByTable = new Map();
|
|
295
373
|
for (const comment of allComments) {
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const
|
|
374
|
+
const owner = this.getValueByIndex(comment, 0);
|
|
375
|
+
const tableName = this.getValueByIndex(comment, 1);
|
|
376
|
+
const columnName = this.getValueByIndex(comment, 2);
|
|
377
|
+
const comments = this.getValueByIndex(comment, 3);
|
|
299
378
|
// 跳过无效数据
|
|
300
379
|
if (!tableName || !columnName || !comments) {
|
|
301
380
|
continue;
|
|
302
381
|
}
|
|
303
|
-
|
|
304
|
-
|
|
382
|
+
const tableKey = this.makeTableKey(owner, tableName, currentUser);
|
|
383
|
+
if (!commentsByTable.has(tableKey)) {
|
|
384
|
+
commentsByTable.set(tableKey, new Map());
|
|
305
385
|
}
|
|
306
|
-
commentsByTable.get(
|
|
386
|
+
commentsByTable.get(tableKey).set(String(columnName).toLowerCase(), String(comments));
|
|
307
387
|
}
|
|
308
388
|
// 将注释添加到列信息中
|
|
309
|
-
for (const [
|
|
310
|
-
const tableComments = commentsByTable.get(
|
|
389
|
+
for (const [tableKey, columns] of columnsByTable.entries()) {
|
|
390
|
+
const tableComments = commentsByTable.get(tableKey);
|
|
311
391
|
if (tableComments) {
|
|
312
392
|
for (const col of columns) {
|
|
313
393
|
if (tableComments.has(col.name)) {
|
|
@@ -317,28 +397,31 @@ export class DMAdapter {
|
|
|
317
397
|
}
|
|
318
398
|
}
|
|
319
399
|
// 按表名分组主键信息
|
|
320
|
-
// 列顺序: 0=
|
|
400
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=COLUMN_NAME, 3=POSITION
|
|
321
401
|
const primaryKeysByTable = new Map();
|
|
322
402
|
for (const pk of allPrimaryKeys) {
|
|
323
|
-
const
|
|
324
|
-
const
|
|
403
|
+
const owner = this.getValueByIndex(pk, 0);
|
|
404
|
+
const tableName = this.getValueByIndex(pk, 1);
|
|
405
|
+
const columnName = this.getValueByIndex(pk, 2);
|
|
325
406
|
// 跳过无效数据
|
|
326
407
|
if (!tableName || !columnName) {
|
|
327
408
|
continue;
|
|
328
409
|
}
|
|
329
|
-
|
|
330
|
-
|
|
410
|
+
const tableKey = this.makeTableKey(owner, tableName, currentUser);
|
|
411
|
+
if (!primaryKeysByTable.has(tableKey)) {
|
|
412
|
+
primaryKeysByTable.set(tableKey, []);
|
|
331
413
|
}
|
|
332
|
-
primaryKeysByTable.get(
|
|
414
|
+
primaryKeysByTable.get(tableKey).push(String(columnName).toLowerCase());
|
|
333
415
|
}
|
|
334
416
|
// 按表名分组索引信息
|
|
335
|
-
// 列顺序: 0=
|
|
417
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=INDEX_NAME, 3=UNIQUENESS, 4=COLUMN_NAME, 5=COLUMN_POSITION
|
|
336
418
|
const indexesByTable = new Map();
|
|
337
419
|
for (const idx of allIndexes) {
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const
|
|
420
|
+
const owner = this.getValueByIndex(idx, 0);
|
|
421
|
+
const tableName = this.getValueByIndex(idx, 1);
|
|
422
|
+
const indexName = this.getValueByIndex(idx, 2);
|
|
423
|
+
const uniqueness = this.getValueByIndex(idx, 3);
|
|
424
|
+
const columnName = this.getValueByIndex(idx, 4);
|
|
342
425
|
// 跳过无效数据
|
|
343
426
|
if (!tableName || !indexName || !columnName) {
|
|
344
427
|
continue;
|
|
@@ -348,10 +431,11 @@ export class DMAdapter {
|
|
|
348
431
|
if (idxNameStr.includes('PK_') || idxNameStr.startsWith('INDEX') || idxNameStr.includes('SYS_')) {
|
|
349
432
|
continue;
|
|
350
433
|
}
|
|
351
|
-
|
|
352
|
-
|
|
434
|
+
const tableKey = this.makeTableKey(owner, tableName, currentUser);
|
|
435
|
+
if (!indexesByTable.has(tableKey)) {
|
|
436
|
+
indexesByTable.set(tableKey, new Map());
|
|
353
437
|
}
|
|
354
|
-
const tableIndexes = indexesByTable.get(
|
|
438
|
+
const tableIndexes = indexesByTable.get(tableKey);
|
|
355
439
|
if (!tableIndexes.has(indexName)) {
|
|
356
440
|
tableIndexes.set(indexName, {
|
|
357
441
|
columns: [],
|
|
@@ -361,41 +445,48 @@ export class DMAdapter {
|
|
|
361
445
|
tableIndexes.get(indexName).columns.push(String(columnName).toLowerCase());
|
|
362
446
|
}
|
|
363
447
|
// 按表名分组行数统计
|
|
364
|
-
// 列顺序: 0=
|
|
448
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=NUM_ROWS, 3=TABLE_COMMENT
|
|
365
449
|
const rowsByTable = new Map();
|
|
366
450
|
const tableCommentsByTable = new Map();
|
|
367
451
|
for (const stat of allStats) {
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const
|
|
452
|
+
const owner = this.getValueByIndex(stat, 0);
|
|
453
|
+
const tableName = this.getValueByIndex(stat, 1);
|
|
454
|
+
const numRows = this.getValueByIndex(stat, 2);
|
|
455
|
+
const tableComment = this.getValueByIndex(stat, 3);
|
|
371
456
|
if (tableName) {
|
|
372
|
-
|
|
457
|
+
const tableKey = this.makeTableKey(owner, tableName, currentUser);
|
|
458
|
+
rowsByTable.set(tableKey, Number(numRows) || 0);
|
|
373
459
|
if (tableComment) {
|
|
374
|
-
tableCommentsByTable.set(
|
|
460
|
+
tableCommentsByTable.set(tableKey, tableComment);
|
|
375
461
|
}
|
|
376
462
|
}
|
|
377
463
|
}
|
|
378
464
|
// 按表名分组外键信息
|
|
379
|
-
// 列顺序: 0=
|
|
465
|
+
// 列顺序: 0=OWNER, 1=TABLE_NAME, 2=CONSTRAINT_NAME, 3=COLUMN_NAME, 4=REFERENCED_TABLE,
|
|
466
|
+
// 5=REFERENCED_COLUMN, 6=DELETE_RULE, 7=REF_OWNER, 8=POSITION
|
|
380
467
|
const foreignKeysByTable = new Map();
|
|
381
468
|
const relationships = [];
|
|
382
469
|
for (const fk of allForeignKeys) {
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
470
|
+
const owner = this.getValueByIndex(fk, 0);
|
|
471
|
+
const tableName = this.getValueByIndex(fk, 1);
|
|
472
|
+
const constraintName = this.getValueByIndex(fk, 2);
|
|
473
|
+
const columnName = this.getValueByIndex(fk, 3);
|
|
474
|
+
const referencedTable = this.getValueByIndex(fk, 4);
|
|
475
|
+
const referencedColumn = this.getValueByIndex(fk, 5);
|
|
476
|
+
const deleteRule = this.getValueByIndex(fk, 6);
|
|
477
|
+
const refOwner = this.getValueByIndex(fk, 7);
|
|
389
478
|
if (!tableName || !constraintName)
|
|
390
479
|
continue;
|
|
391
|
-
|
|
392
|
-
|
|
480
|
+
const tableKey = this.makeTableKey(owner, tableName, currentUser);
|
|
481
|
+
const refTableKey = this.makeTableKey(refOwner, referencedTable, currentUser);
|
|
482
|
+
if (!foreignKeysByTable.has(tableKey)) {
|
|
483
|
+
foreignKeysByTable.set(tableKey, new Map());
|
|
393
484
|
}
|
|
394
|
-
const tableForeignKeys = foreignKeysByTable.get(
|
|
485
|
+
const tableForeignKeys = foreignKeysByTable.get(tableKey);
|
|
395
486
|
if (!tableForeignKeys.has(constraintName)) {
|
|
396
487
|
tableForeignKeys.set(constraintName, {
|
|
397
488
|
columns: [],
|
|
398
|
-
referencedTable: String(
|
|
489
|
+
referencedTable: String(refTableKey).toLowerCase(),
|
|
399
490
|
referencedColumns: [],
|
|
400
491
|
onDelete: deleteRule,
|
|
401
492
|
});
|
|
@@ -405,10 +496,10 @@ export class DMAdapter {
|
|
|
405
496
|
fkInfo.referencedColumns.push(String(referencedColumn).toLowerCase());
|
|
406
497
|
}
|
|
407
498
|
// 生成全局关系视图
|
|
408
|
-
for (const [
|
|
499
|
+
for (const [tableKey, tableForeignKeys] of foreignKeysByTable.entries()) {
|
|
409
500
|
for (const [constraintName, fkInfo] of tableForeignKeys.entries()) {
|
|
410
501
|
relationships.push({
|
|
411
|
-
fromTable: String(
|
|
502
|
+
fromTable: String(tableKey).toLowerCase(),
|
|
412
503
|
fromColumns: fkInfo.columns,
|
|
413
504
|
toTable: fkInfo.referencedTable,
|
|
414
505
|
toColumns: fkInfo.referencedColumns,
|
|
@@ -419,8 +510,8 @@ export class DMAdapter {
|
|
|
419
510
|
}
|
|
420
511
|
// 组装表信息
|
|
421
512
|
const tableInfos = [];
|
|
422
|
-
for (const [
|
|
423
|
-
const tableIndexes = indexesByTable.get(
|
|
513
|
+
for (const [tableKey, columns] of columnsByTable.entries()) {
|
|
514
|
+
const tableIndexes = indexesByTable.get(tableKey);
|
|
424
515
|
const indexInfos = [];
|
|
425
516
|
if (tableIndexes) {
|
|
426
517
|
for (const [indexName, indexData] of tableIndexes.entries()) {
|
|
@@ -432,7 +523,7 @@ export class DMAdapter {
|
|
|
432
523
|
}
|
|
433
524
|
}
|
|
434
525
|
// 组装外键信息
|
|
435
|
-
const tableForeignKeys = foreignKeysByTable.get(
|
|
526
|
+
const tableForeignKeys = foreignKeysByTable.get(tableKey);
|
|
436
527
|
const foreignKeyInfos = [];
|
|
437
528
|
if (tableForeignKeys) {
|
|
438
529
|
for (const [constraintName, fkData] of tableForeignKeys.entries()) {
|
|
@@ -446,13 +537,14 @@ export class DMAdapter {
|
|
|
446
537
|
}
|
|
447
538
|
}
|
|
448
539
|
tableInfos.push({
|
|
449
|
-
name: String(
|
|
450
|
-
|
|
540
|
+
name: String(tableKey).toLowerCase(),
|
|
541
|
+
schema: schemaByTable.get(tableKey),
|
|
542
|
+
comment: tableCommentsByTable.get(tableKey) || undefined,
|
|
451
543
|
columns,
|
|
452
|
-
primaryKeys: primaryKeysByTable.get(
|
|
544
|
+
primaryKeys: primaryKeysByTable.get(tableKey) || [],
|
|
453
545
|
indexes: indexInfos,
|
|
454
546
|
foreignKeys: foreignKeyInfos.length > 0 ? foreignKeyInfos : undefined,
|
|
455
|
-
estimatedRows: rowsByTable.get(
|
|
547
|
+
estimatedRows: rowsByTable.get(tableKey) || 0,
|
|
456
548
|
});
|
|
457
549
|
}
|
|
458
550
|
// 按表名排序
|