fdb2 1.0.33 → 1.0.35
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 +4 -2
- package/dist/public/explorer.css +152 -272
- package/dist/public/explorer.js +354 -465
- package/dist/public/index.js +3 -1
- package/dist/public/layout.css +265 -54
- package/dist/public/layout.js +184 -19
- package/dist/public/vue.js +1 -1
- package/dist/server/core/db-api.js +5 -0
- package/dist/server/core/db-api.js.map +1 -1
- package/dist/server/core/db-api.ts +5 -0
- package/dist/server/core/dialect.d.ts +1 -1
- package/dist/server/core/dialect.d.ts.map +1 -1
- package/dist/server/core/dialect.js +21 -1
- package/dist/server/core/dialect.js.map +1 -1
- package/dist/server/core/dialect.ts +23 -2
- package/dist/server/service/connection.service.d.ts.map +1 -1
- package/dist/server/service/connection.service.js +17 -1
- package/dist/server/service/connection.service.js.map +1 -1
- package/dist/server/service/connection.service.ts +17 -1
- package/dist/server/service/database/clickhouse-connection.d.ts +44 -0
- package/dist/server/service/database/clickhouse-connection.d.ts.map +1 -0
- package/dist/server/service/database/clickhouse-connection.js +210 -0
- package/dist/server/service/database/clickhouse-connection.js.map +1 -0
- package/dist/server/service/database/clickhouse-connection.ts +182 -0
- package/dist/server/service/database/clickhouse.service.d.ts +138 -0
- package/dist/server/service/database/clickhouse.service.d.ts.map +1 -0
- package/dist/server/service/database/clickhouse.service.js +794 -0
- package/dist/server/service/database/clickhouse.service.js.map +1 -0
- package/dist/server/service/database/clickhouse.service.ts +853 -0
- package/dist/server/service/database/database.service.d.ts +42 -0
- package/dist/server/service/database/database.service.d.ts.map +1 -1
- package/dist/server/service/database/database.service.js +37 -0
- package/dist/server/service/database/database.service.js.map +1 -1
- package/dist/server/service/database/database.service.ts +37 -0
- package/dist/server/service/database/index.d.ts +1 -0
- package/dist/server/service/database/index.d.ts.map +1 -1
- package/dist/server/service/database/index.js +3 -1
- package/dist/server/service/database/index.js.map +1 -1
- package/dist/server/service/database/index.ts +1 -0
- package/package.json +1 -4
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ClickHouseService = void 0;
|
|
37
|
+
const base_service_1 = require("./base.service");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const clickhouse_connection_1 = require("./clickhouse-connection");
|
|
41
|
+
/**
|
|
42
|
+
* ClickHouse数据库服务实现
|
|
43
|
+
* ClickHouse 是列式 OLAP 数据库,使用 HTTP 接口(默认 8123)。
|
|
44
|
+
* 注意:ClickHouse 不支持事务、外键、存储过程,主键通过 ORDER BY 体现。
|
|
45
|
+
*/
|
|
46
|
+
class ClickHouseService extends base_service_1.BaseDatabaseService {
|
|
47
|
+
getDatabaseType() {
|
|
48
|
+
return 'clickhouse';
|
|
49
|
+
}
|
|
50
|
+
escape(value) {
|
|
51
|
+
return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 获取ClickHouse数据库列表
|
|
55
|
+
*/
|
|
56
|
+
async getDatabases(dataSource) {
|
|
57
|
+
try {
|
|
58
|
+
const result = await dataSource.query(`SHOW DATABASES`);
|
|
59
|
+
return (result || [])
|
|
60
|
+
.map((row) => (typeof row === 'string' ? row : row.name))
|
|
61
|
+
.filter((name) => name && name !== 'system');
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('获取ClickHouse数据库列表失败:', error);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 获取ClickHouse表/视图列表
|
|
70
|
+
*/
|
|
71
|
+
async getTables(dataSource, database) {
|
|
72
|
+
try {
|
|
73
|
+
const result = await dataSource.query(`SELECT name, engine, total_rows
|
|
74
|
+
FROM system.tables
|
|
75
|
+
WHERE database = '${this.escape(database)}'
|
|
76
|
+
ORDER BY name`);
|
|
77
|
+
// 各表大小使用独立查询获取:不同 ClickHouse 版本 system.tables 的列名不同
|
|
78
|
+
// (bytes_on_disk / total_bytes 等),获取失败不应影响表列表本身。
|
|
79
|
+
const sizeMap = {};
|
|
80
|
+
try {
|
|
81
|
+
const sizes = await dataSource.query(`SELECT name, total_bytes FROM system.tables WHERE database = '${this.escape(database)}'`);
|
|
82
|
+
(sizes || []).forEach((r) => {
|
|
83
|
+
if (r.name != null && r.total_bytes != null)
|
|
84
|
+
sizeMap[r.name] = Number(r.total_bytes);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* 大小列在当前版本不存在,忽略即可 */
|
|
89
|
+
}
|
|
90
|
+
return (result || []).map((row) => {
|
|
91
|
+
const engine = (row.engine || '').toUpperCase();
|
|
92
|
+
const isView = engine === 'VIEW' || engine === 'MATERIALIZEDVIEW' || engine === 'LIVEVIEW';
|
|
93
|
+
return {
|
|
94
|
+
name: row.name,
|
|
95
|
+
type: isView ? 'view' : 'table',
|
|
96
|
+
engine: row.engine,
|
|
97
|
+
comment: '',
|
|
98
|
+
rowCount: row.total_rows != null ? Number(row.total_rows) : undefined,
|
|
99
|
+
dataSize: sizeMap[row.name] != null ? sizeMap[row.name] : undefined,
|
|
100
|
+
indexSize: undefined
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('获取ClickHouse表列表失败:', error);
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 获取表数据(分页)。
|
|
111
|
+
* 必须使用 `库`.`表` 限定名,因为连接默认库可能与目标库不同。
|
|
112
|
+
*/
|
|
113
|
+
async getTableData(dataSource, databaseName, tableName, page = 1, pageSize = 100, where, orderBy) {
|
|
114
|
+
const qualified = `\`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\``;
|
|
115
|
+
let query = `SELECT * FROM ${qualified}`;
|
|
116
|
+
if (where)
|
|
117
|
+
query += ` WHERE ${where}`;
|
|
118
|
+
if (orderBy)
|
|
119
|
+
query += ` ORDER BY ${orderBy}`;
|
|
120
|
+
const offset = (page - 1) * pageSize;
|
|
121
|
+
query += ` LIMIT ${pageSize} OFFSET ${offset}`;
|
|
122
|
+
const data = await dataSource.query(query);
|
|
123
|
+
let countQuery = `SELECT COUNT(*) AS total FROM ${qualified}`;
|
|
124
|
+
if (where)
|
|
125
|
+
countQuery += ` WHERE ${where}`;
|
|
126
|
+
const countResult = await dataSource.query(countQuery);
|
|
127
|
+
const total = Number((countResult && countResult[0] && countResult[0].total) || 0);
|
|
128
|
+
return { data, total };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 获取ClickHouse列信息
|
|
132
|
+
*/
|
|
133
|
+
async getColumns(dataSource, database, table) {
|
|
134
|
+
try {
|
|
135
|
+
const result = await dataSource.query(`SELECT name, type, default_kind, default_expression, comment, is_in_primary_key
|
|
136
|
+
FROM system.columns
|
|
137
|
+
WHERE database = '${this.escape(database)}' AND table = '${this.escape(table)}'
|
|
138
|
+
ORDER BY position`);
|
|
139
|
+
return (result || []).map((row) => {
|
|
140
|
+
const type = row.type || '';
|
|
141
|
+
// ClickHouse 的可空性体现在类型上,如 Nullable(Int32);system.columns 没有稳定的 is_nullable 列
|
|
142
|
+
const nullable = /nullable\s*\(/i.test(type);
|
|
143
|
+
return {
|
|
144
|
+
name: row.name,
|
|
145
|
+
type,
|
|
146
|
+
nullable,
|
|
147
|
+
defaultValue: row.default_expression || undefined,
|
|
148
|
+
comment: row.comment || '',
|
|
149
|
+
isPrimary: row.is_in_primary_key === 1 || row.is_in_primary_key === true || row.is_in_primary_key === '1',
|
|
150
|
+
isAutoIncrement: false
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error('获取ClickHouse列信息失败:', error);
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 获取ClickHouse索引信息(主键 + 数据跳数索引)
|
|
161
|
+
*/
|
|
162
|
+
async getIndexes(dataSource, database, table) {
|
|
163
|
+
try {
|
|
164
|
+
const indexes = [];
|
|
165
|
+
// 主键
|
|
166
|
+
const pkResult = await dataSource.query(`SELECT name FROM system.columns
|
|
167
|
+
WHERE database = '${this.escape(database)}' AND table = '${this.escape(table)}' AND is_in_primary_key = 1
|
|
168
|
+
ORDER BY position`);
|
|
169
|
+
const pkColumns = (pkResult || []).map((r) => r.name);
|
|
170
|
+
if (pkColumns.length > 0) {
|
|
171
|
+
indexes.push({ name: 'primary', type: 'PRIMARY', columns: pkColumns, unique: true });
|
|
172
|
+
}
|
|
173
|
+
// 数据跳数索引(部分版本/视图下该系统表可能不存在,失败时不影响主键结果)
|
|
174
|
+
try {
|
|
175
|
+
const skipResult = await dataSource.query(`SELECT name, type, expr, granularity
|
|
176
|
+
FROM system.data_skipping_indexes
|
|
177
|
+
WHERE database = '${this.escape(database)}' AND table = '${this.escape(table)}'`);
|
|
178
|
+
for (const row of (skipResult || [])) {
|
|
179
|
+
indexes.push({
|
|
180
|
+
name: row.name,
|
|
181
|
+
type: row.type || 'SKIPPING',
|
|
182
|
+
columns: row.expr ? [row.expr] : [],
|
|
183
|
+
unique: false
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (skipError) {
|
|
188
|
+
console.warn('获取ClickHouse数据跳数索引失败,已忽略:', skipError?.message || skipError);
|
|
189
|
+
}
|
|
190
|
+
return indexes;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.error('获取ClickHouse索引信息失败:', error);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* ClickHouse不支持外键
|
|
199
|
+
*/
|
|
200
|
+
async getForeignKeys(dataSource, database, table) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 获取ClickHouse数据库大小
|
|
205
|
+
*/
|
|
206
|
+
async getDatabaseSize(dataSource, database) {
|
|
207
|
+
try {
|
|
208
|
+
const result = await dataSource.query(`SELECT COALESCE(sum(total_bytes), 0) as size FROM system.tables WHERE database = '${this.escape(database)}'`);
|
|
209
|
+
return Number((result && result[0] && result[0].size) || 0);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error('获取ClickHouse数据库大小失败:', error);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 获取ClickHouse视图列表
|
|
218
|
+
*/
|
|
219
|
+
async getViews(dataSource, database) {
|
|
220
|
+
try {
|
|
221
|
+
const result = await dataSource.query(`SELECT name, engine, as_select FROM system.tables
|
|
222
|
+
WHERE database = '${this.escape(database)}' AND engine IN ('View', 'MaterializedView', 'LiveView')
|
|
223
|
+
ORDER BY name`);
|
|
224
|
+
return (result || []).map((row) => ({
|
|
225
|
+
name: row.name,
|
|
226
|
+
comment: '',
|
|
227
|
+
schemaName: database,
|
|
228
|
+
definition: row.as_select || '',
|
|
229
|
+
engine: row.engine
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.error('获取ClickHouse视图列表失败:', error);
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 获取ClickHouse视图定义
|
|
239
|
+
*/
|
|
240
|
+
async getViewDefinition(dataSource, database, viewName) {
|
|
241
|
+
try {
|
|
242
|
+
const result = await dataSource.query(`SELECT as_select FROM system.tables WHERE database = '${this.escape(database)}' AND name = '${this.escape(viewName)}'`);
|
|
243
|
+
return (result && result[0] && result[0].as_select) || '';
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
console.error('获取ClickHouse视图定义失败:', error);
|
|
247
|
+
return '';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* ClickHouse不支持存储过程
|
|
252
|
+
*/
|
|
253
|
+
async getProcedures(dataSource, database) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
async getProcedureDefinition(dataSource, database, procedureName) {
|
|
257
|
+
throw new Error('ClickHouse不支持存储过程');
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 创建ClickHouse数据库
|
|
261
|
+
*/
|
|
262
|
+
async createDatabase(dataSource, databaseName, options) {
|
|
263
|
+
await dataSource.query(`CREATE DATABASE IF NOT EXISTS \`${this.escape(databaseName)}\``);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 删除ClickHouse数据库
|
|
267
|
+
*/
|
|
268
|
+
async dropDatabase(dataSource, databaseName) {
|
|
269
|
+
await dataSource.query(`DROP DATABASE IF EXISTS \`${this.escape(databaseName)}\``);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 导出数据库架构
|
|
273
|
+
*/
|
|
274
|
+
async exportSchema(dataSource, databaseName) {
|
|
275
|
+
const tables = await this.getTables(dataSource, databaseName);
|
|
276
|
+
let schemaSql = `-- ClickHouse数据库架构导出 - ${databaseName}\n`;
|
|
277
|
+
schemaSql += `-- 导出时间: ${new Date().toISOString()}\n\n`;
|
|
278
|
+
for (const table of tables) {
|
|
279
|
+
try {
|
|
280
|
+
const result = await dataSource.query(`SHOW CREATE TABLE \`${this.escape(databaseName)}\`.\`${this.escape(table.name)}\``);
|
|
281
|
+
const statement = (result && result[0] && (result[0].statement || result[0].create_table_query)) || '';
|
|
282
|
+
schemaSql += `-- 表/视图: ${table.name}\n`;
|
|
283
|
+
schemaSql += `${statement};\n\n`;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
schemaSql += `-- 表 ${table.name} 导出失败: ${error?.message || error}\n\n`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return schemaSql;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 查看ClickHouse日志(query_log,可能未开启)
|
|
293
|
+
*/
|
|
294
|
+
async viewLogs(dataSource, database, limit = 100) {
|
|
295
|
+
try {
|
|
296
|
+
const result = await dataSource.query(`SELECT event_time, user, query_kind, query, exception
|
|
297
|
+
FROM system.query_log
|
|
298
|
+
ORDER BY event_time DESC
|
|
299
|
+
LIMIT ${Number(limit) || 100}`);
|
|
300
|
+
return result || [];
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
console.error('获取ClickHouse日志失败:', error);
|
|
304
|
+
return [{ message: 'ClickHouse日志功能需要开启 query_log(通常仅在配置了 query_log 的集群可用)' }];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 备份ClickHouse数据库
|
|
309
|
+
*/
|
|
310
|
+
async backupDatabase(dataSource, databaseName, options) {
|
|
311
|
+
try {
|
|
312
|
+
const backupPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'backups');
|
|
313
|
+
if (!fs.existsSync(backupPath)) {
|
|
314
|
+
fs.mkdirSync(backupPath, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
317
|
+
const backupFile = path.join(backupPath, `${databaseName}_${timestamp}.sql`);
|
|
318
|
+
// 1) 结构(含视图)
|
|
319
|
+
const schema = await this.exportSchema(dataSource, databaseName);
|
|
320
|
+
fs.writeFileSync(backupFile, schema, 'utf8');
|
|
321
|
+
// 2) 数据(仅普通表,视图无数据)。includeData=false 时仅备份结构
|
|
322
|
+
const includeData = options?.includeData !== false;
|
|
323
|
+
if (includeData) {
|
|
324
|
+
const tables = (await this.getTables(dataSource, databaseName)).filter((t) => t.type === 'table');
|
|
325
|
+
for (const t of tables) {
|
|
326
|
+
await this.appendTableData(dataSource, databaseName, t.name, backupFile);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return `备份成功:${backupFile}`;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
console.error('ClickHouse备份失败:', error);
|
|
333
|
+
throw new Error(`备份失败: ${error?.message || error}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 将单表数据以 INSERT 语句追加到备份文件中。
|
|
338
|
+
* 使用 JSONStringsEachRow(见连接封装),UInt64 等大整数以字符串形式安全导出。
|
|
339
|
+
*/
|
|
340
|
+
async appendTableData(dataSource, databaseName, tableName, file) {
|
|
341
|
+
const columns = await this.getColumns(dataSource, databaseName, tableName);
|
|
342
|
+
const columnNames = columns.map((c) => c.name);
|
|
343
|
+
const colList = columnNames.map((c) => `\`${this.escape(c)}\``).join(', ');
|
|
344
|
+
fs.appendFileSync(file, `\n-- 数据: ${tableName}\n`, 'utf8');
|
|
345
|
+
const batchSize = 10000;
|
|
346
|
+
let offset = 0;
|
|
347
|
+
let hasMore = true;
|
|
348
|
+
while (hasMore) {
|
|
349
|
+
const query = `SELECT * FROM \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\` LIMIT ${batchSize} OFFSET ${offset}`;
|
|
350
|
+
const data = await dataSource.query(query);
|
|
351
|
+
if (!data || data.length === 0) {
|
|
352
|
+
hasMore = false;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const qualifiedTable = `\`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\``;
|
|
356
|
+
let batch = '';
|
|
357
|
+
for (const row of data) {
|
|
358
|
+
const values = columnNames
|
|
359
|
+
.map((column) => clickhouse_connection_1.ClickHouseConnection.escapeLiteral(row[column]))
|
|
360
|
+
.join(', ');
|
|
361
|
+
batch += `INSERT INTO ${qualifiedTable} (${colList}) VALUES (${values});\n`;
|
|
362
|
+
}
|
|
363
|
+
fs.appendFileSync(file, batch, 'utf8');
|
|
364
|
+
offset += batchSize;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* 将 SQL 文本拆分为语句,正确处理单/双引号、反引号标识符与 -- /* 注释中的分号。
|
|
369
|
+
*/
|
|
370
|
+
static splitSqlStatements(text) {
|
|
371
|
+
const statements = [];
|
|
372
|
+
let cur = '';
|
|
373
|
+
let inSingle = false;
|
|
374
|
+
let inDouble = false;
|
|
375
|
+
let inBacktick = false;
|
|
376
|
+
let inLineComment = false;
|
|
377
|
+
let inBlockComment = false;
|
|
378
|
+
for (let i = 0; i < text.length; i++) {
|
|
379
|
+
const ch = text[i];
|
|
380
|
+
const next = text[i + 1];
|
|
381
|
+
if (inLineComment) {
|
|
382
|
+
cur += ch;
|
|
383
|
+
if (ch === '\n')
|
|
384
|
+
inLineComment = false;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (inBlockComment) {
|
|
388
|
+
if (ch === '*' && next === '/') {
|
|
389
|
+
i++;
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
cur += ch;
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (inSingle) {
|
|
397
|
+
cur += ch;
|
|
398
|
+
// ClickHouse 以反斜杠转义单引号(\'),此时引号不结束字符串
|
|
399
|
+
if (ch === '\\' && next !== undefined) {
|
|
400
|
+
cur += next;
|
|
401
|
+
i++;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (ch === "'")
|
|
405
|
+
inSingle = false;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (inDouble) {
|
|
409
|
+
cur += ch;
|
|
410
|
+
if (ch === '"')
|
|
411
|
+
inDouble = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (inBacktick) {
|
|
415
|
+
cur += ch;
|
|
416
|
+
if (ch === '`')
|
|
417
|
+
inBacktick = false;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (ch === '-' && next === '-') {
|
|
421
|
+
inLineComment = true;
|
|
422
|
+
cur += ch;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (ch === '/' && next === '*') {
|
|
426
|
+
inBlockComment = true;
|
|
427
|
+
i++;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (ch === "'") {
|
|
431
|
+
inSingle = true;
|
|
432
|
+
cur += ch;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (ch === '"') {
|
|
436
|
+
inDouble = true;
|
|
437
|
+
cur += ch;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (ch === '`') {
|
|
441
|
+
inBacktick = true;
|
|
442
|
+
cur += ch;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (ch === ';') {
|
|
446
|
+
const trimmed = cur.trim();
|
|
447
|
+
if (trimmed)
|
|
448
|
+
statements.push(trimmed);
|
|
449
|
+
cur = '';
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
cur += ch;
|
|
453
|
+
}
|
|
454
|
+
const last = cur.trim();
|
|
455
|
+
if (last)
|
|
456
|
+
statements.push(last);
|
|
457
|
+
return statements;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 恢复ClickHouse数据库
|
|
461
|
+
*/
|
|
462
|
+
async restoreDatabase(dataSource, databaseName, filePath, options) {
|
|
463
|
+
try {
|
|
464
|
+
const sqlContent = fs.readFileSync(filePath, 'utf8');
|
|
465
|
+
// 使用引号/注释感知的切分,避免字符串或视图定义中的分号被错误截断
|
|
466
|
+
const statements = ClickHouseService.splitSqlStatements(sqlContent);
|
|
467
|
+
for (const statement of statements) {
|
|
468
|
+
await dataSource.query(statement);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
console.error('ClickHouse恢复失败:', error);
|
|
473
|
+
throw new Error(`恢复失败: ${error?.message || error}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* 导出表数据到 SQL 文件
|
|
478
|
+
*/
|
|
479
|
+
async exportTableDataToSQL(dataSource, databaseName, tableName, options) {
|
|
480
|
+
try {
|
|
481
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
482
|
+
if (!fs.existsSync(exportPath)) {
|
|
483
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
484
|
+
}
|
|
485
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
486
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.sql`);
|
|
487
|
+
const columns = await this.getColumns(dataSource, databaseName, tableName);
|
|
488
|
+
const columnNames = columns.map((c) => c.name);
|
|
489
|
+
const header = `-- 表数据导出 - ${tableName}\n` + `-- 导出时间: ${new Date().toISOString()}\n\n`;
|
|
490
|
+
fs.writeFileSync(exportFile, header, 'utf8');
|
|
491
|
+
const batchSize = options?.batchSize || 10000;
|
|
492
|
+
let offset = 0;
|
|
493
|
+
let hasMoreData = true;
|
|
494
|
+
while (hasMoreData) {
|
|
495
|
+
const query = `SELECT * FROM \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\` LIMIT ${batchSize} OFFSET ${offset}`;
|
|
496
|
+
const data = await dataSource.query(query);
|
|
497
|
+
if (!data || data.length === 0) {
|
|
498
|
+
hasMoreData = false;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
const qualifiedTable = `\`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\``;
|
|
502
|
+
let batchSql = '';
|
|
503
|
+
for (const row of data) {
|
|
504
|
+
const values = columnNames.map((column) => clickhouse_connection_1.ClickHouseConnection.escapeLiteral(row[column]));
|
|
505
|
+
batchSql += `INSERT INTO ${qualifiedTable} (${columnNames.map((c) => `\`${this.escape(c)}\``).join(', ')}) VALUES (${values.join(', ')});\n`;
|
|
506
|
+
}
|
|
507
|
+
fs.appendFileSync(exportFile, batchSql, 'utf8');
|
|
508
|
+
offset += batchSize;
|
|
509
|
+
console.log(`ClickHouse导出表数据进度: ${tableName} - 已处理 ${offset} 行`);
|
|
510
|
+
}
|
|
511
|
+
return exportFile;
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
console.error('ClickHouse导出表数据到SQL失败:', error);
|
|
515
|
+
throw new Error(`导出表数据到SQL失败: ${error?.message || error}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* 导出表数据到 CSV 文件
|
|
520
|
+
*/
|
|
521
|
+
async exportTableDataToCSV(dataSource, databaseName, tableName, options) {
|
|
522
|
+
try {
|
|
523
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
524
|
+
if (!fs.existsSync(exportPath)) {
|
|
525
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
526
|
+
}
|
|
527
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
528
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.csv`);
|
|
529
|
+
const columns = await this.getColumns(dataSource, databaseName, tableName);
|
|
530
|
+
const columnNames = columns.map((c) => c.name);
|
|
531
|
+
const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
532
|
+
fs.writeFileSync(exportFile, bom);
|
|
533
|
+
fs.appendFileSync(exportFile, columnNames.map((name) => `"${name}"`).join(',') + '\n', 'utf8');
|
|
534
|
+
const batchSize = options?.batchSize || 10000;
|
|
535
|
+
let offset = 0;
|
|
536
|
+
let hasMoreData = true;
|
|
537
|
+
while (hasMoreData) {
|
|
538
|
+
const query = `SELECT * FROM \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\` LIMIT ${batchSize} OFFSET ${offset}`;
|
|
539
|
+
const data = await dataSource.query(query);
|
|
540
|
+
if (!data || data.length === 0) {
|
|
541
|
+
hasMoreData = false;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
let batchCsv = '';
|
|
545
|
+
for (const row of data) {
|
|
546
|
+
const values = columnNames.map((column) => {
|
|
547
|
+
const value = row[column];
|
|
548
|
+
if (value === null || value === undefined) {
|
|
549
|
+
return '';
|
|
550
|
+
}
|
|
551
|
+
else if (typeof value === 'string') {
|
|
552
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
553
|
+
}
|
|
554
|
+
else if (value instanceof Date) {
|
|
555
|
+
return `"${value.toISOString()}"`;
|
|
556
|
+
}
|
|
557
|
+
else if (typeof value === 'object' && value !== null) {
|
|
558
|
+
try {
|
|
559
|
+
return `"${JSON.stringify(value).replace(/"/g, '""')}"`;
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return `"${String(value).replace(/"/g, '""')}"`;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
return String(value);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
batchCsv += values.join(',') + '\n';
|
|
570
|
+
}
|
|
571
|
+
fs.appendFileSync(exportFile, batchCsv, 'utf8');
|
|
572
|
+
offset += batchSize;
|
|
573
|
+
console.log(`ClickHouse导出表数据到CSV进度: ${tableName} - 已处理 ${offset} 行`);
|
|
574
|
+
}
|
|
575
|
+
return exportFile;
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
console.error('ClickHouse导出表数据到CSV失败:', error);
|
|
579
|
+
throw new Error(`导出表数据到CSV失败: ${error?.message || error}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* 导出表数据到 JSON 文件
|
|
584
|
+
*/
|
|
585
|
+
async exportTableDataToJSON(dataSource, databaseName, tableName, options) {
|
|
586
|
+
try {
|
|
587
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
588
|
+
if (!fs.existsSync(exportPath)) {
|
|
589
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
590
|
+
}
|
|
591
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
592
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.json`);
|
|
593
|
+
const batchSize = options?.batchSize || 10000;
|
|
594
|
+
let offset = 0;
|
|
595
|
+
let hasMoreData = true;
|
|
596
|
+
let allData = [];
|
|
597
|
+
while (hasMoreData) {
|
|
598
|
+
const query = `SELECT * FROM \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\` LIMIT ${batchSize} OFFSET ${offset}`;
|
|
599
|
+
const data = await dataSource.query(query);
|
|
600
|
+
if (!data || data.length === 0) {
|
|
601
|
+
hasMoreData = false;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
allData = allData.concat(data);
|
|
605
|
+
offset += batchSize;
|
|
606
|
+
console.log(`ClickHouse导出表数据到JSON进度: ${tableName} - 已处理 ${offset} 行`);
|
|
607
|
+
}
|
|
608
|
+
fs.writeFileSync(exportFile, JSON.stringify(allData, null, 2), 'utf8');
|
|
609
|
+
return exportFile;
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
console.error('ClickHouse导出表数据到JSON失败:', error);
|
|
613
|
+
throw new Error(`导出表数据到JSON失败: ${error?.message || error}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* 导出表数据到 Excel 文件
|
|
618
|
+
*/
|
|
619
|
+
async exportTableDataToExcel(dataSource, databaseName, tableName, options) {
|
|
620
|
+
try {
|
|
621
|
+
const exportPath = options?.path || path.join(__dirname, '..', '..', '..', 'data', 'exports');
|
|
622
|
+
if (!fs.existsSync(exportPath)) {
|
|
623
|
+
fs.mkdirSync(exportPath, { recursive: true });
|
|
624
|
+
}
|
|
625
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
626
|
+
const exportFile = path.join(exportPath, `${tableName}_data_${timestamp}.xlsx`);
|
|
627
|
+
const columns = await this.getColumns(dataSource, databaseName, tableName);
|
|
628
|
+
const columnNames = columns.map((c) => c.name);
|
|
629
|
+
const batchSize = options?.batchSize || 10000;
|
|
630
|
+
let offset = 0;
|
|
631
|
+
let hasMoreData = true;
|
|
632
|
+
let allData = [];
|
|
633
|
+
while (hasMoreData) {
|
|
634
|
+
const query = `SELECT * FROM \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\` LIMIT ${batchSize} OFFSET ${offset}`;
|
|
635
|
+
const data = await dataSource.query(query);
|
|
636
|
+
if (!data || data.length === 0) {
|
|
637
|
+
hasMoreData = false;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
allData = allData.concat(data);
|
|
641
|
+
offset += batchSize;
|
|
642
|
+
console.log(`ClickHouse导出表数据到Excel进度: ${tableName} - 已处理 ${offset} 行`);
|
|
643
|
+
}
|
|
644
|
+
const ExcelJS = require('exceljs');
|
|
645
|
+
const workbook = new ExcelJS.Workbook();
|
|
646
|
+
const worksheet = workbook.addWorksheet(tableName);
|
|
647
|
+
worksheet.columns = columnNames.map((name) => ({ header: name, key: name }));
|
|
648
|
+
worksheet.addRows(allData);
|
|
649
|
+
await workbook.xlsx.writeFile(exportFile);
|
|
650
|
+
return exportFile;
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
console.error('ClickHouse导出表数据到Excel失败:', error);
|
|
654
|
+
throw new Error(`导出表数据到Excel失败: ${error?.message || error}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* ClickHouse使用反引号作为标识符
|
|
659
|
+
*/
|
|
660
|
+
quoteIdentifier(identifier) {
|
|
661
|
+
return `\`${identifier.replace(/`/g, '``')}\``;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* 解析列类型:可空且类型本身不是 Nullable(...) 时包装 Nullable;
|
|
665
|
+
* 不附加 NOT NULL,避免与 Nullable(...) 冲突(ClickHouse 默认即非空)。
|
|
666
|
+
*/
|
|
667
|
+
resolveColumnType(column) {
|
|
668
|
+
const type = column.type || 'String';
|
|
669
|
+
if (column.nullable && !/nullable\s*\(/i.test(type)) {
|
|
670
|
+
return `Nullable(${type})`;
|
|
671
|
+
}
|
|
672
|
+
return type;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* 修改表结构
|
|
676
|
+
* ClickHouse支持 ADD/MODIFY/DROP/COMMENT COLUMN,但不支持事务。
|
|
677
|
+
*/
|
|
678
|
+
async alterTable(dataSource, databaseName, tableDiff) {
|
|
679
|
+
try {
|
|
680
|
+
const tableName = tableDiff.tableName;
|
|
681
|
+
const qualified = `\`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\``;
|
|
682
|
+
const sqlStatements = [];
|
|
683
|
+
// 添加新列
|
|
684
|
+
(tableDiff.addedColumns || []).forEach((column) => {
|
|
685
|
+
let columnSQL = `ALTER TABLE ${qualified} ADD COLUMN \`${this.escape(column.name)}\` ${this.resolveColumnType(column)}`;
|
|
686
|
+
if (column.defaultValue) {
|
|
687
|
+
const upperDefault = column.defaultValue.toString().toUpperCase();
|
|
688
|
+
if (['CURRENT_TIMESTAMP', 'NOW()', 'TODAY()'].includes(upperDefault)) {
|
|
689
|
+
columnSQL += ` DEFAULT ${upperDefault}`;
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
columnSQL += ` DEFAULT ${clickhouse_connection_1.ClickHouseConnection.escapeLiteral(column.defaultValue)}`;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
columnSQL += ';';
|
|
696
|
+
sqlStatements.push(columnSQL);
|
|
697
|
+
if (column.comment) {
|
|
698
|
+
sqlStatements.push(`ALTER TABLE ${qualified} COMMENT COLUMN \`${this.escape(column.name)}\` '${this.escape(column.comment)}';`);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// 修改列
|
|
702
|
+
(tableDiff.modifiedColumns || []).forEach((modification) => {
|
|
703
|
+
const { newColumn } = modification;
|
|
704
|
+
sqlStatements.push(`ALTER TABLE ${qualified} MODIFY COLUMN \`${this.escape(newColumn.name)}\` ${this.resolveColumnType(newColumn)};`);
|
|
705
|
+
if (newColumn.comment) {
|
|
706
|
+
sqlStatements.push(`ALTER TABLE ${qualified} COMMENT COLUMN \`${this.escape(newColumn.name)}\` '${this.escape(newColumn.comment)}';`);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
// 删除列
|
|
710
|
+
(tableDiff.deletedColumns || []).forEach((column) => {
|
|
711
|
+
sqlStatements.push(`ALTER TABLE ${qualified} DROP COLUMN \`${this.escape(column.name)}\`;`);
|
|
712
|
+
});
|
|
713
|
+
for (const statement of sqlStatements) {
|
|
714
|
+
await dataSource.query(statement);
|
|
715
|
+
}
|
|
716
|
+
return { ret: 0, message: '表结构修改成功' };
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
console.error('ClickHouse修改表结构失败:', error);
|
|
720
|
+
return { ret: 1, message: `修改表结构失败: ${error?.message || error}` };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* 批量插入数据
|
|
725
|
+
*/
|
|
726
|
+
async bulkInsert(dataSource, databaseName, tableName, data) {
|
|
727
|
+
if (!data || data.length === 0)
|
|
728
|
+
return;
|
|
729
|
+
const columns = Object.keys(data[0]);
|
|
730
|
+
const rowsSql = data.map((row) => `(${columns.map((column) => clickhouse_connection_1.ClickHouseConnection.escapeLiteral(row[column])).join(', ')})`).join(', ');
|
|
731
|
+
const sql = `INSERT INTO \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\` (${columns.map((c) => `\`${this.escape(c)}\``).join(', ')}) VALUES ${rowsSql}`;
|
|
732
|
+
await dataSource.query(sql);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* 插入单条数据
|
|
736
|
+
*/
|
|
737
|
+
async insertData(dataSource, databaseName, tableName, data) {
|
|
738
|
+
const columns = Object.keys(data);
|
|
739
|
+
const values = columns.map((column) => clickhouse_connection_1.ClickHouseConnection.escapeLiteral(data[column]));
|
|
740
|
+
const sql = `INSERT INTO \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\` (${columns.map((c) => `\`${this.escape(c)}\``).join(', ')}) VALUES (${values.join(', ')})`;
|
|
741
|
+
await dataSource.query(sql);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* 删除表
|
|
745
|
+
*/
|
|
746
|
+
async dropTable(dataSource, databaseName, tableName) {
|
|
747
|
+
await dataSource.query(`DROP TABLE IF EXISTS \`${this.escape(databaseName)}\`.\`${this.escape(tableName)}\``);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* 创建表
|
|
751
|
+
* ClickHouse要求指定 ENGINE,默认使用 MergeTree。
|
|
752
|
+
*/
|
|
753
|
+
async createTable(dataSource, databaseName, table) {
|
|
754
|
+
const { name, columns, comment } = table;
|
|
755
|
+
const qualified = `\`${this.escape(databaseName)}\`.\`${this.escape(name)}\``;
|
|
756
|
+
const primaryKeys = [];
|
|
757
|
+
const columnDefs = [];
|
|
758
|
+
(columns || []).forEach((column) => {
|
|
759
|
+
const type = column.type || 'String';
|
|
760
|
+
let columnDef = ` \`${this.escape(column.name)}\` ${type}`;
|
|
761
|
+
// 仅当列可空且类型本身不是 Nullable(...) 时才包装 Nullable,避免 NOT NULL 与 Nullable 冲突
|
|
762
|
+
const isAlreadyNullable = /nullable\s*\(/i.test(type);
|
|
763
|
+
if (column.nullable && !isAlreadyNullable) {
|
|
764
|
+
columnDef = ` \`${this.escape(column.name)}\` Nullable(${type})`;
|
|
765
|
+
}
|
|
766
|
+
if (column.defaultValue) {
|
|
767
|
+
const upperDefault = column.defaultValue.toString().toUpperCase();
|
|
768
|
+
if (['CURRENT_TIMESTAMP', 'NOW()', 'TODAY()'].includes(upperDefault)) {
|
|
769
|
+
columnDef += ` DEFAULT ${upperDefault}`;
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
columnDef += ` DEFAULT ${clickhouse_connection_1.ClickHouseConnection.escapeLiteral(column.defaultValue)}`;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (column.isPrimary) {
|
|
776
|
+
primaryKeys.push(`\`${this.escape(column.name)}\``);
|
|
777
|
+
}
|
|
778
|
+
if (column.comment) {
|
|
779
|
+
columnDef += ` COMMENT '${this.escape(column.comment)}'`;
|
|
780
|
+
}
|
|
781
|
+
columnDefs.push(columnDef);
|
|
782
|
+
});
|
|
783
|
+
let sql = `CREATE TABLE IF NOT EXISTS ${qualified} (\n${columnDefs.join(',\n')}\n) ENGINE = MergeTree()`;
|
|
784
|
+
// ClickHouse 中 PRIMARY KEY/ORDER BY 必须位于 ENGINE 之后;主键默认等于 ORDER BY,这里只保留 ORDER BY
|
|
785
|
+
sql += primaryKeys.length > 0 ? ` ORDER BY (${primaryKeys.join(', ')})` : ' ORDER BY tuple()';
|
|
786
|
+
if (comment) {
|
|
787
|
+
sql += ` COMMENT '${this.escape(comment)}'`;
|
|
788
|
+
}
|
|
789
|
+
sql += ';';
|
|
790
|
+
await dataSource.query(sql);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
exports.ClickHouseService = ClickHouseService;
|
|
794
|
+
//# sourceMappingURL=clickhouse.service.js.map
|