fdb2 1.0.0 → 1.0.2
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 +334 -221
- package/bin/fdb2.js +39 -22
- package/package.json +2 -2
- package/server/service/connection.service.ts +8 -1
- package/server/service/database/cockroachdb.service.ts +659 -0
- package/server/service/database/database.service.ts +120 -0
- package/server/service/database/mongodb.service.ts +454 -0
- package/server/service/database/oracle.service.ts +7 -14
- package/server/service/database/postgres.service.ts +4 -7
- package/server/service/database/sap.service.ts +713 -0
- package/server/service/database/sqlite.service.ts +3 -3
- package/server.js +23 -1
- package/server.pid +1 -0
- package/src/platform/database/components/database-detail.vue +11 -11
- package/src/platform/database/components/table-detail.vue +4 -4
- package/src/platform/database/types/common.ts +1 -1
- package/vite.config.ts +1 -1
- package/bin/docker/.env +0 -4
- package/data/connections.demo.json +0 -32
- package/nw-build.js +0 -120
- package/nw-dev.js +0 -65
- package/src/base//345/237/272/347/241/200/345/261/202.md +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdb2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "view/index.html",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"dayjs": "^1.11.19",
|
|
43
43
|
"express": "^5.2.1",
|
|
44
44
|
"mysql2": "^3.16.2",
|
|
45
|
+
"oracledb": "^6.10.0",
|
|
45
46
|
"pg": "^8.17.2",
|
|
46
|
-
"pm2": "^6.0.14",
|
|
47
47
|
"reflect-metadata": "^0.2.2",
|
|
48
48
|
"sqlite3": "^5.1.7",
|
|
49
49
|
"typeorm": "^0.3.28"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
3
4
|
import { ConnectionEntity } from '../model/connection.entity';
|
|
4
5
|
import { DataSource, type DataSourceOptions } from 'typeorm';
|
|
5
6
|
|
|
@@ -11,14 +12,20 @@ export class ConnectionService {
|
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 连接配置文件路径
|
|
15
|
+
* 优先使用环境变量 DB_TOOL_DATA_DIR,否则使用用户主目录下的 .db-tool 目录
|
|
14
16
|
*/
|
|
15
|
-
private readonly configPath
|
|
17
|
+
private readonly configPath: string;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* 活跃的数据库连接实例
|
|
19
21
|
*/
|
|
20
22
|
private activeConnections: Map<string, DataSource> = new Map();
|
|
21
23
|
|
|
24
|
+
constructor() {
|
|
25
|
+
const dataDir = process.env.DB_TOOL_DATA_DIR || path.join(os.homedir(), '.fdb2');
|
|
26
|
+
this.configPath = path.join(dataDir, 'connections.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
/**
|
|
23
30
|
* 初始化服务,创建配置目录
|
|
24
31
|
*/
|
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { DataSource } from 'typeorm';
|
|
2
|
+
import { BaseDatabaseService } from './base.service';
|
|
3
|
+
import {
|
|
4
|
+
DatabaseEntity,
|
|
5
|
+
TableEntity,
|
|
6
|
+
ColumnEntity,
|
|
7
|
+
IndexEntity,
|
|
8
|
+
ForeignKeyEntity
|
|
9
|
+
} from '../../model/database.entity';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CockroachDB数据库服务实现
|
|
15
|
+
* CockroachDB 是一个兼容 PostgreSQL 的分布式 SQL 数据库
|
|
16
|
+
*/
|
|
17
|
+
export class CockroachDBService extends BaseDatabaseService {
|
|
18
|
+
|
|
19
|
+
getDatabaseType() {
|
|
20
|
+
return 'cockroachdb';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取CockroachDB数据库列表
|
|
25
|
+
*/
|
|
26
|
+
async getDatabases(dataSource: DataSource): Promise<string[]> {
|
|
27
|
+
const result = await dataSource.query(`
|
|
28
|
+
SELECT
|
|
29
|
+
database_name as name
|
|
30
|
+
FROM
|
|
31
|
+
information_schema.databases
|
|
32
|
+
WHERE
|
|
33
|
+
database_name NOT IN ('system', 'pg_catalog', 'information_schema', 'crdb_internal')
|
|
34
|
+
ORDER BY
|
|
35
|
+
database_name
|
|
36
|
+
`);
|
|
37
|
+
return result.map((row: any) => row.name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 获取CockroachDB表列表
|
|
42
|
+
*/
|
|
43
|
+
async getTables(dataSource: DataSource, database: string): Promise<TableEntity[]> {
|
|
44
|
+
const result = await dataSource.query(`
|
|
45
|
+
SELECT
|
|
46
|
+
t.table_name as name,
|
|
47
|
+
'table' as type,
|
|
48
|
+
obj_description(t.oid) as comment
|
|
49
|
+
FROM
|
|
50
|
+
information_schema.tables t
|
|
51
|
+
WHERE
|
|
52
|
+
t.table_schema = $1
|
|
53
|
+
AND t.table_type = 'BASE TABLE'
|
|
54
|
+
ORDER BY
|
|
55
|
+
t.table_name
|
|
56
|
+
`, [database]);
|
|
57
|
+
|
|
58
|
+
return result.map((row: any) => ({
|
|
59
|
+
name: row.name,
|
|
60
|
+
type: row.type,
|
|
61
|
+
comment: row.comment || '',
|
|
62
|
+
rowCount: undefined,
|
|
63
|
+
dataSize: undefined,
|
|
64
|
+
indexSize: undefined
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 获取CockroachDB列信息
|
|
70
|
+
*/
|
|
71
|
+
async getColumns(dataSource: DataSource, database: string, table: string): Promise<ColumnEntity[]> {
|
|
72
|
+
const result = await dataSource.query(`
|
|
73
|
+
SELECT
|
|
74
|
+
column_name as name,
|
|
75
|
+
data_type as type,
|
|
76
|
+
is_nullable as nullable,
|
|
77
|
+
column_default as defaultValue,
|
|
78
|
+
CASE
|
|
79
|
+
WHEN column_name IN (
|
|
80
|
+
SELECT column_name
|
|
81
|
+
FROM information_schema.key_column_usage
|
|
82
|
+
WHERE table_schema = $1
|
|
83
|
+
AND table_name = $2
|
|
84
|
+
AND constraint_name LIKE '%_pkey'
|
|
85
|
+
) THEN true
|
|
86
|
+
ELSE false
|
|
87
|
+
END as isPrimary,
|
|
88
|
+
CASE
|
|
89
|
+
WHEN data_type LIKE '%serial%' OR data_type LIKE '%identity%'
|
|
90
|
+
THEN true
|
|
91
|
+
ELSE false
|
|
92
|
+
END as isAutoIncrement
|
|
93
|
+
FROM
|
|
94
|
+
information_schema.columns
|
|
95
|
+
WHERE
|
|
96
|
+
table_schema = $1
|
|
97
|
+
AND table_name = $2
|
|
98
|
+
ORDER BY
|
|
99
|
+
ordinal_position
|
|
100
|
+
`, [database, table]);
|
|
101
|
+
|
|
102
|
+
return result.map((row: any) => ({
|
|
103
|
+
name: row.name,
|
|
104
|
+
type: row.type,
|
|
105
|
+
nullable: row.nullable === 'YES',
|
|
106
|
+
defaultValue: row.defaultValue,
|
|
107
|
+
isPrimary: row.isPrimary,
|
|
108
|
+
isAutoIncrement: row.isAutoIncrement
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 获取CockroachDB索引信息
|
|
114
|
+
*/
|
|
115
|
+
async getIndexes(dataSource: DataSource, database: string, table: string): Promise<IndexEntity[]> {
|
|
116
|
+
const result = await dataSource.query(`
|
|
117
|
+
SELECT
|
|
118
|
+
i.relname as name,
|
|
119
|
+
CASE
|
|
120
|
+
WHEN i.indisunique THEN 'UNIQUE'
|
|
121
|
+
ELSE 'INDEX'
|
|
122
|
+
END as type,
|
|
123
|
+
array_agg(a.attname ORDER BY k.n) as columns,
|
|
124
|
+
i.indisunique as unique
|
|
125
|
+
FROM
|
|
126
|
+
pg_index i
|
|
127
|
+
JOIN pg_class t ON t.oid = i.indrelid
|
|
128
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
129
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(i.indkey)
|
|
130
|
+
CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(n, ord) ON true
|
|
131
|
+
WHERE
|
|
132
|
+
n.nspname = $1
|
|
133
|
+
AND t.relname = $2
|
|
134
|
+
GROUP BY
|
|
135
|
+
i.relname, i.indisunique
|
|
136
|
+
ORDER BY
|
|
137
|
+
i.relname
|
|
138
|
+
`, [database, table]);
|
|
139
|
+
|
|
140
|
+
return result.map((row: any) => ({
|
|
141
|
+
name: row.name,
|
|
142
|
+
type: row.type,
|
|
143
|
+
columns: row.columns || [],
|
|
144
|
+
unique: row.unique
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 获取CockroachDB外键信息
|
|
150
|
+
*/
|
|
151
|
+
async getForeignKeys(dataSource: DataSource, database: string, table: string): Promise<ForeignKeyEntity[]> {
|
|
152
|
+
const result = await dataSource.query(`
|
|
153
|
+
SELECT
|
|
154
|
+
tc.constraint_name as name,
|
|
155
|
+
kcu.column_name as column,
|
|
156
|
+
ccu.table_name as referencedTable,
|
|
157
|
+
ccu.column_name as referencedColumn,
|
|
158
|
+
rc.update_rule as onUpdate,
|
|
159
|
+
rc.delete_rule as onDelete
|
|
160
|
+
FROM
|
|
161
|
+
information_schema.table_constraints tc
|
|
162
|
+
JOIN information_schema.key_column_usage kcu
|
|
163
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
164
|
+
AND tc.table_schema = kcu.table_schema
|
|
165
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
166
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
167
|
+
AND ccu.table_schema = tc.table_schema
|
|
168
|
+
JOIN information_schema.referential_constraints rc
|
|
169
|
+
ON rc.constraint_name = tc.constraint_name
|
|
170
|
+
WHERE
|
|
171
|
+
tc.constraint_type = 'FOREIGN KEY'
|
|
172
|
+
AND tc.table_schema = $1
|
|
173
|
+
AND tc.table_name = $2
|
|
174
|
+
ORDER BY
|
|
175
|
+
tc.constraint_name
|
|
176
|
+
`, [database, table]);
|
|
177
|
+
|
|
178
|
+
return result.map((row: any) => ({
|
|
179
|
+
name: row.name,
|
|
180
|
+
column: row.column,
|
|
181
|
+
referencedTable: row.referencedTable,
|
|
182
|
+
referencedColumn: row.referencedColumn,
|
|
183
|
+
onDelete: row.onDelete || 'NO ACTION',
|
|
184
|
+
onUpdate: row.onUpdate || 'NO ACTION'
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 获取CockroachDB数据库大小
|
|
190
|
+
*/
|
|
191
|
+
async getDatabaseSize(dataSource: DataSource, database: string): Promise<number> {
|
|
192
|
+
try {
|
|
193
|
+
const result = await dataSource.query(`
|
|
194
|
+
SELECT
|
|
195
|
+
pg_database_size(datname) as size
|
|
196
|
+
FROM
|
|
197
|
+
pg_database
|
|
198
|
+
WHERE
|
|
199
|
+
datname = $1
|
|
200
|
+
`, [database]);
|
|
201
|
+
return result[0]?.size || 0;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 获取CockroachDB视图列表
|
|
209
|
+
*/
|
|
210
|
+
async getViews(dataSource: DataSource, database: string): Promise<any[]> {
|
|
211
|
+
const result = await dataSource.query(`
|
|
212
|
+
SELECT
|
|
213
|
+
table_name as name,
|
|
214
|
+
view_definition as definition
|
|
215
|
+
FROM
|
|
216
|
+
information_schema.views
|
|
217
|
+
WHERE
|
|
218
|
+
table_schema = $1
|
|
219
|
+
ORDER BY
|
|
220
|
+
table_name
|
|
221
|
+
`, [database]);
|
|
222
|
+
|
|
223
|
+
return result.map((row: any) => ({
|
|
224
|
+
name: row.name,
|
|
225
|
+
comment: '',
|
|
226
|
+
schemaName: database,
|
|
227
|
+
definition: row.definition
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 获取CockroachDB视图定义
|
|
233
|
+
*/
|
|
234
|
+
async getViewDefinition(dataSource: DataSource, database: string, viewName: string): Promise<string> {
|
|
235
|
+
const result = await dataSource.query(`
|
|
236
|
+
SELECT
|
|
237
|
+
view_definition as definition
|
|
238
|
+
FROM
|
|
239
|
+
information_schema.views
|
|
240
|
+
WHERE
|
|
241
|
+
table_schema = $1
|
|
242
|
+
AND table_name = $2
|
|
243
|
+
`, [database, viewName]);
|
|
244
|
+
return result[0]?.definition || '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 获取CockroachDB存储过程列表
|
|
249
|
+
*/
|
|
250
|
+
async getProcedures(dataSource: DataSource, database: string): Promise<any[]> {
|
|
251
|
+
const result = await dataSource.query(`
|
|
252
|
+
SELECT
|
|
253
|
+
p.proname as name,
|
|
254
|
+
pg_get_userbyid(p.proowner).usename as owner,
|
|
255
|
+
pg_get_functiondef(p.oid) as definition
|
|
256
|
+
FROM
|
|
257
|
+
pg_proc p
|
|
258
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
259
|
+
WHERE
|
|
260
|
+
n.nspname = $1
|
|
261
|
+
AND p.prokind = 'f'
|
|
262
|
+
ORDER BY
|
|
263
|
+
p.proname
|
|
264
|
+
`, [database]);
|
|
265
|
+
|
|
266
|
+
return result.map((row: any) => ({
|
|
267
|
+
name: row.name,
|
|
268
|
+
owner: row.owner,
|
|
269
|
+
definition: row.definition
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 获取CockroachDB存储过程定义
|
|
275
|
+
*/
|
|
276
|
+
async getProcedureDefinition(dataSource: DataSource, database: string, procedureName: string): Promise<string> {
|
|
277
|
+
const result = await dataSource.query(`
|
|
278
|
+
SELECT
|
|
279
|
+
pg_get_functiondef(p.oid) as definition
|
|
280
|
+
FROM
|
|
281
|
+
pg_proc p
|
|
282
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
283
|
+
WHERE
|
|
284
|
+
n.nspname = $1
|
|
285
|
+
AND p.proname = $2
|
|
286
|
+
`, [database, procedureName]);
|
|
287
|
+
return result[0]?.definition || '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 创建CockroachDB数据库
|
|
292
|
+
*/
|
|
293
|
+
async createDatabase(dataSource: DataSource, databaseName: string, options?: any): Promise<void> {
|
|
294
|
+
await dataSource.query(`CREATE DATABASE ${this.quoteIdentifier(databaseName)}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 删除CockroachDB数据库
|
|
299
|
+
*/
|
|
300
|
+
async dropDatabase(dataSource: DataSource, databaseName: string): Promise<void> {
|
|
301
|
+
await dataSource.query(`DROP DATABASE IF EXISTS ${this.quoteIdentifier(databaseName)}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 导出数据库架构
|
|
306
|
+
*/
|
|
307
|
+
async exportSchema(dataSource: DataSource, databaseName: string): Promise<string> {
|
|
308
|
+
const tables = await this.getTables(dataSource, databaseName);
|
|
309
|
+
let schemaSql = `-- CockroachDB数据库架构导出 - ${databaseName}\n`;
|
|
310
|
+
schemaSql += `-- 导出时间: ${new Date().toISOString()}\n\n`;
|
|
311
|
+
|
|
312
|
+
for (const table of tables) {
|
|
313
|
+
const columns = await this.getColumns(dataSource, databaseName, table.name);
|
|
314
|
+
const indexes = await this.getIndexes(dataSource, databaseName, table.name);
|
|
315
|
+
const foreignKeys = await this.getForeignKeys(dataSource, databaseName, table.name);
|
|
316
|
+
|
|
317
|
+
schemaSql += `-- 表结构: ${table.name}\n`;
|
|
318
|
+
schemaSql += `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(table.name)} (\n`;
|
|
319
|
+
|
|
320
|
+
const columnDefinitions = columns.map(column => {
|
|
321
|
+
let definition = ` ${this.quoteIdentifier(column.name)} ${column.type}`;
|
|
322
|
+
if (!column.nullable)
|
|
323
|
+
definition += ' NOT NULL';
|
|
324
|
+
if (column.defaultValue !== undefined) {
|
|
325
|
+
const upperDefault = column.defaultValue.toString().toUpperCase();
|
|
326
|
+
if (upperDefault === 'CURRENT_TIMESTAMP' || upperDefault === 'NOW()' || upperDefault === 'CURRENT_DATE') {
|
|
327
|
+
definition += ` DEFAULT ${upperDefault}`;
|
|
328
|
+
} else {
|
|
329
|
+
definition += ` DEFAULT ${column.defaultValue === null ? 'NULL' : `'${column.defaultValue}'`}`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (column.isPrimary)
|
|
333
|
+
definition += ' PRIMARY KEY';
|
|
334
|
+
return definition;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
schemaSql += columnDefinitions.join(',\n');
|
|
338
|
+
schemaSql += '\n);\n\n';
|
|
339
|
+
|
|
340
|
+
for (const index of indexes) {
|
|
341
|
+
if (index.type === 'PRIMARY' || index.name.toUpperCase() === 'PRIMARY')
|
|
342
|
+
continue;
|
|
343
|
+
schemaSql += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(table.name)} (${index.columns.map(col => this.quoteIdentifier(col)).join(', ')});\n`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (indexes.length > 0)
|
|
347
|
+
schemaSql += '\n';
|
|
348
|
+
|
|
349
|
+
for (const foreignKey of foreignKeys) {
|
|
350
|
+
schemaSql += `ALTER TABLE ${this.quoteIdentifier(table.name)} ADD CONSTRAINT ${this.quoteIdentifier(foreignKey.name)} FOREIGN KEY (${this.quoteIdentifier(foreignKey.column)}) REFERENCES ${this.quoteIdentifier(foreignKey.referencedTable)} (${this.quoteIdentifier(foreignKey.referencedColumn)})${foreignKey.onDelete ? ` ON DELETE ${foreignKey.onDelete}` : ''}${foreignKey.onUpdate ? ` ON UPDATE ${foreignKey.onUpdate}` : ''};\n`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (foreignKeys.length > 0)
|
|
354
|
+
schemaSql += '\n';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return schemaSql;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 查看CockroachDB日志
|
|
362
|
+
*/
|
|
363
|
+
async viewLogs(dataSource: DataSource, database?: string, limit: number = 100): Promise<any[]> {
|
|
364
|
+
try {
|
|
365
|
+
const result = await dataSource.query(`
|
|
366
|
+
SELECT
|
|
367
|
+
log_time,
|
|
368
|
+
user_name,
|
|
369
|
+
database_name,
|
|
370
|
+
error_severity,
|
|
371
|
+
message
|
|
372
|
+
FROM
|
|
373
|
+
crdb_internal.statement_statistics
|
|
374
|
+
ORDER BY
|
|
375
|
+
log_time DESC
|
|
376
|
+
LIMIT $1
|
|
377
|
+
`, [limit]);
|
|
378
|
+
return result;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return [{ message: 'CockroachDB日志功能有限,请使用 SHOW CLUSTER SETTINGS 查看配置' }];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 备份CockroachDB数据库
|
|
386
|
+
*/
|
|
387
|
+
async backupDatabase(dataSource: DataSource, databaseName: string, options?: any): Promise<string> {
|
|
388
|
+
try {
|
|
389
|
+
const backupPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'backups');
|
|
390
|
+
if (!fs.existsSync(backupPath)) {
|
|
391
|
+
fs.mkdirSync(backupPath, { recursive: true });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
395
|
+
const backupFile = path.join(backupPath, `${databaseName}_${timestamp}.sql`);
|
|
396
|
+
|
|
397
|
+
const schema = await this.exportSchema(dataSource, databaseName);
|
|
398
|
+
fs.writeFileSync(backupFile, schema, 'utf8');
|
|
399
|
+
|
|
400
|
+
return `备份成功:${backupFile}`;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error('CockroachDB备份失败:', error);
|
|
403
|
+
throw new Error(`备份失败: ${error.message}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 恢复CockroachDB数据库
|
|
409
|
+
*/
|
|
410
|
+
async restoreDatabase(dataSource: DataSource, databaseName: string, filePath: string, options?: any): Promise<void> {
|
|
411
|
+
try {
|
|
412
|
+
const sqlContent = fs.readFileSync(filePath, 'utf8');
|
|
413
|
+
await this.executeSqlFile(dataSource, filePath);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error('CockroachDB恢复失败:', error);
|
|
416
|
+
throw new Error(`恢复失败: ${error.message}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 导出表数据到 SQL 文件
|
|
422
|
+
*/
|
|
423
|
+
async exportTableDataToSQL(dataSource: DataSource, databaseName: string, tableName: string, options?: any): Promise<string> {
|
|
424
|
+
try {
|
|
425
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
426
|
+
if (!fs.existsSync(exportPath)) {
|
|
427
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
431
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.sql`);
|
|
432
|
+
|
|
433
|
+
const columns = await this.getColumns(dataSource, databaseName, tableName);
|
|
434
|
+
const columnNames = columns.map(column => column.name);
|
|
435
|
+
|
|
436
|
+
const header = `-- 表数据导出 - ${tableName}\n` +
|
|
437
|
+
`-- 导出时间: ${new Date().toISOString()}\n\n`;
|
|
438
|
+
fs.writeFileSync(exportFile, header, 'utf8');
|
|
439
|
+
|
|
440
|
+
const batchSize = options?.batchSize || 10000;
|
|
441
|
+
let offset = 0;
|
|
442
|
+
let hasMoreData = true;
|
|
443
|
+
|
|
444
|
+
while (hasMoreData) {
|
|
445
|
+
const query = `SELECT * FROM ${this.quoteIdentifier(tableName)} LIMIT ${batchSize} OFFSET ${offset}`;
|
|
446
|
+
const data = await dataSource.query(query);
|
|
447
|
+
|
|
448
|
+
if (data.length === 0) {
|
|
449
|
+
hasMoreData = false;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let batchSql = '';
|
|
454
|
+
data.forEach((row) => {
|
|
455
|
+
const values = columnNames.map(column => {
|
|
456
|
+
const value = row[column];
|
|
457
|
+
if (value === null || value === undefined) {
|
|
458
|
+
return 'NULL';
|
|
459
|
+
} else if (typeof value === 'string') {
|
|
460
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
461
|
+
} else if (typeof value === 'boolean') {
|
|
462
|
+
return value ? 'true' : 'false';
|
|
463
|
+
} else if (value instanceof Date) {
|
|
464
|
+
return `'${value.toISOString()}'`;
|
|
465
|
+
} else if (typeof value === 'object') {
|
|
466
|
+
try {
|
|
467
|
+
const stringValue = JSON.stringify(value);
|
|
468
|
+
return `'${stringValue.replace(/'/g, "''")}'`;
|
|
469
|
+
} catch {
|
|
470
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
return String(value);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
batchSql += `INSERT INTO ${this.quoteIdentifier(tableName)} (${columnNames.map(col => this.quoteIdentifier(col)).join(', ')}) VALUES (${values.join(', ')});\n`;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
fs.appendFileSync(exportFile, batchSql, 'utf8');
|
|
480
|
+
offset += batchSize;
|
|
481
|
+
console.log(`CockroachDB导出表数据进度: ${tableName} - 已处理 ${offset} 行`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return exportFile;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error('CockroachDB导出表数据失败:', error);
|
|
487
|
+
throw new Error(`导出表数据失败: ${error.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 导出表数据到 CSV 文件
|
|
493
|
+
*/
|
|
494
|
+
async exportTableDataToCSV(dataSource: DataSource, databaseName: string, tableName: string, options?: any): Promise<string> {
|
|
495
|
+
try {
|
|
496
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
497
|
+
if (!fs.existsSync(exportPath)) {
|
|
498
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
502
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.csv`);
|
|
503
|
+
|
|
504
|
+
const columns = await this.getColumns(dataSource, databaseName, tableName);
|
|
505
|
+
const columnNames = columns.map(column => column.name);
|
|
506
|
+
|
|
507
|
+
const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
508
|
+
fs.writeFileSync(exportFile, bom);
|
|
509
|
+
fs.appendFileSync(exportFile, columnNames.map(name => `"${name}"`).join(',') + '\n', 'utf8');
|
|
510
|
+
|
|
511
|
+
const batchSize = options?.batchSize || 10000;
|
|
512
|
+
let offset = 0;
|
|
513
|
+
let hasMoreData = true;
|
|
514
|
+
|
|
515
|
+
while (hasMoreData) {
|
|
516
|
+
const query = `SELECT * FROM ${this.quoteIdentifier(tableName)} LIMIT ${batchSize} OFFSET ${offset}`;
|
|
517
|
+
const data = await dataSource.query(query);
|
|
518
|
+
|
|
519
|
+
if (data.length === 0) {
|
|
520
|
+
hasMoreData = false;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let batchCsv = '';
|
|
525
|
+
data.forEach((row) => {
|
|
526
|
+
const values = columnNames.map(column => {
|
|
527
|
+
const value = row[column];
|
|
528
|
+
if (value === null || value === undefined) {
|
|
529
|
+
return '';
|
|
530
|
+
} else if (typeof value === 'string') {
|
|
531
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
532
|
+
} else if (value instanceof Date) {
|
|
533
|
+
return `"${value.toISOString()}"`;
|
|
534
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
535
|
+
try {
|
|
536
|
+
return `"${JSON.stringify(value).replace(/"/g, '""')}"`;
|
|
537
|
+
} catch {
|
|
538
|
+
return `"${String(value).replace(/"/g, '""')}"`;
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
return String(value);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
batchCsv += values.join(',') + '\n';
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
fs.appendFileSync(exportFile, batchCsv, 'utf8');
|
|
548
|
+
offset += batchSize;
|
|
549
|
+
console.log(`CockroachDB导出表数据到CSV进度: ${tableName} - 已处理 ${offset} 行`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return exportFile;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
console.error('CockroachDB导出表数据到CSV失败:', error);
|
|
555
|
+
throw new Error(`导出表数据到CSV失败: ${error.message}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 导出表数据到 JSON 文件
|
|
561
|
+
*/
|
|
562
|
+
async exportTableDataToJSON(dataSource: DataSource, databaseName: string, tableName: string, options?: any): Promise<string> {
|
|
563
|
+
try {
|
|
564
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
565
|
+
if (!fs.existsSync(exportPath)) {
|
|
566
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
570
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.json`);
|
|
571
|
+
|
|
572
|
+
const batchSize = options?.batchSize || 10000;
|
|
573
|
+
let offset = 0;
|
|
574
|
+
let hasMoreData = true;
|
|
575
|
+
let allData: any[] = [];
|
|
576
|
+
|
|
577
|
+
while (hasMoreData) {
|
|
578
|
+
const query = `SELECT * FROM ${this.quoteIdentifier(tableName)} LIMIT ${batchSize} OFFSET ${offset}`;
|
|
579
|
+
const data = await dataSource.query(query);
|
|
580
|
+
|
|
581
|
+
if (data.length === 0) {
|
|
582
|
+
hasMoreData = false;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
allData = allData.concat(data);
|
|
587
|
+
offset += batchSize;
|
|
588
|
+
console.log(`CockroachDB导出表数据到JSON进度: ${tableName} - 已处理 ${offset} 行`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
fs.writeFileSync(exportFile, JSON.stringify(allData, null, 2), 'utf8');
|
|
592
|
+
return exportFile;
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.error('CockroachDB导出表数据到JSON失败:', error);
|
|
595
|
+
throw new Error(`导出表数据到JSON失败: ${error.message}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* 导出表数据到 Excel 文件
|
|
601
|
+
*/
|
|
602
|
+
async exportTableDataToExcel(dataSource: DataSource, databaseName: string, tableName: string, options?: any): Promise<string> {
|
|
603
|
+
try {
|
|
604
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
605
|
+
if (!fs.existsSync(exportPath)) {
|
|
606
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
610
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.xlsx`);
|
|
611
|
+
|
|
612
|
+
const columns = await this.getColumns(dataSource, databaseName, tableName);
|
|
613
|
+
const columnNames = columns.map(column => column.name);
|
|
614
|
+
|
|
615
|
+
const batchSize = options?.batchSize || 10000;
|
|
616
|
+
let offset = 0;
|
|
617
|
+
let hasMoreData = true;
|
|
618
|
+
let allData: any[] = [];
|
|
619
|
+
|
|
620
|
+
while (hasMoreData) {
|
|
621
|
+
const query = `SELECT * FROM ${this.quoteIdentifier(tableName)} LIMIT ${batchSize} OFFSET ${offset}`;
|
|
622
|
+
const data = await dataSource.query(query);
|
|
623
|
+
|
|
624
|
+
if (data.length === 0) {
|
|
625
|
+
hasMoreData = false;
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
allData = allData.concat(data);
|
|
630
|
+
offset += batchSize;
|
|
631
|
+
console.log(`CockroachDB导出表数据到Excel进度: ${tableName} - 已处理 ${offset} 行`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const ExcelJS = require('exceljs');
|
|
635
|
+
const workbook = new ExcelJS.Workbook();
|
|
636
|
+
const worksheet = workbook.addWorksheet(tableName);
|
|
637
|
+
|
|
638
|
+
worksheet.columns = columnNames.map(name => ({
|
|
639
|
+
header: name,
|
|
640
|
+
key: name
|
|
641
|
+
}));
|
|
642
|
+
|
|
643
|
+
worksheet.addRows(allData);
|
|
644
|
+
|
|
645
|
+
await workbook.xlsx.writeFile(exportFile);
|
|
646
|
+
return exportFile;
|
|
647
|
+
} catch (error) {
|
|
648
|
+
console.error('CockroachDB导出表数据到Excel失败:', error);
|
|
649
|
+
throw new Error(`导出表数据到Excel失败: ${error.message}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* CockroachDB使用双引号作为标识符
|
|
655
|
+
*/
|
|
656
|
+
public quoteIdentifier(identifier: string): string {
|
|
657
|
+
return `"${identifier}"`;
|
|
658
|
+
}
|
|
659
|
+
}
|