dbgate-api-premium 7.1.13 → 7.2.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/package.json +6 -6
- package/src/controllers/databaseConnections.js +15 -0
- package/src/controllers/sessions.js +64 -0
- package/src/currentVersion.js +2 -2
- package/src/proc/databaseConnectionProcess.js +56 -1
- package/src/proc/sessionProcess.js +72 -5
- package/src/utility/handleQueryStream.js +108 -21
- package/src/utility/queryResultMetadata.js +70 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbgate-api-premium",
|
|
3
3
|
"main": "src/index.js",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.2.0",
|
|
5
5
|
"homepage": "https://www.dbgate.io/",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"compare-versions": "^3.6.0",
|
|
31
31
|
"cors": "^2.8.5",
|
|
32
32
|
"cross-env": "^6.0.3",
|
|
33
|
-
"dbgate-datalib": "7.
|
|
33
|
+
"dbgate-datalib": "7.2.0",
|
|
34
34
|
"dbgate-query-splitter": "^4.12.0",
|
|
35
|
-
"dbgate-rest": "7.
|
|
36
|
-
"dbgate-sqltree": "7.
|
|
37
|
-
"dbgate-tools": "7.
|
|
35
|
+
"dbgate-rest": "7.2.0",
|
|
36
|
+
"dbgate-sqltree": "7.2.0",
|
|
37
|
+
"dbgate-tools": "7.2.0",
|
|
38
38
|
"debug": "^4.3.4",
|
|
39
39
|
"diff": "^5.0.0",
|
|
40
40
|
"diff2html": "^3.4.13",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"@types/fs-extra": "^9.0.11",
|
|
97
97
|
"@types/jest": "^30.0.0",
|
|
98
98
|
"@types/lodash": "^4.14.149",
|
|
99
|
-
"dbgate-types": "7.
|
|
99
|
+
"dbgate-types": "7.2.0",
|
|
100
100
|
"env-cmd": "^10.1.0",
|
|
101
101
|
"jest": "^30.4.2",
|
|
102
102
|
"jsdoc-to-markdown": "^9.0.5",
|
|
@@ -517,6 +517,21 @@ module.exports = {
|
|
|
517
517
|
return res.result || null;
|
|
518
518
|
},
|
|
519
519
|
|
|
520
|
+
saveQueryResultData_meta: true,
|
|
521
|
+
async saveQueryResultData({ conid, database, changeSet, sql }, req) {
|
|
522
|
+
await testConnectionPermission(conid, req);
|
|
523
|
+
await testDatabaseRolePermission(conid, database, 'run_script', req);
|
|
524
|
+
|
|
525
|
+
const opened = await this.ensureOpened(conid, database);
|
|
526
|
+
const res = await this.sendRequest(opened, { msgtype: 'saveQueryResultData', changeSet, sql });
|
|
527
|
+
if (res.errorMessage) {
|
|
528
|
+
return {
|
|
529
|
+
errorMessage: res.errorMessage,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return res.result || { state: 'ok' };
|
|
533
|
+
},
|
|
534
|
+
|
|
520
535
|
multiCallMethod_meta: true,
|
|
521
536
|
async multiCallMethod({ conid, database, callList }, req) {
|
|
522
537
|
await testConnectionPermission(conid, req);
|
|
@@ -21,6 +21,7 @@ const logger = getLogger('sessions');
|
|
|
21
21
|
module.exports = {
|
|
22
22
|
/** @type {import('dbgate-types').OpenedSession[]} */
|
|
23
23
|
opened: [],
|
|
24
|
+
requests: {},
|
|
24
25
|
|
|
25
26
|
// handle_error(sesid, props) {
|
|
26
27
|
// const { error } = props;
|
|
@@ -115,6 +116,44 @@ module.exports = {
|
|
|
115
116
|
|
|
116
117
|
handle_ping() {},
|
|
117
118
|
|
|
119
|
+
handle_response(sesid, props) {
|
|
120
|
+
const { msgid } = props;
|
|
121
|
+
const request = this.requests[msgid];
|
|
122
|
+
if (!request) return;
|
|
123
|
+
delete this.requests[msgid];
|
|
124
|
+
request.resolve(_.omit(props, ['msgtype', 'msgid', 'sesid']));
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
sendRequest(session, message) {
|
|
128
|
+
const msgid = crypto.randomUUID();
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
session.subprocess.off('exit', handleExit);
|
|
132
|
+
delete this.requests[msgid];
|
|
133
|
+
};
|
|
134
|
+
const handleExit = () => {
|
|
135
|
+
cleanup();
|
|
136
|
+
reject(new Error('DBGM-00000 Session process exited before response was received'));
|
|
137
|
+
};
|
|
138
|
+
this.requests[msgid] = {
|
|
139
|
+
resolve: value => {
|
|
140
|
+
cleanup();
|
|
141
|
+
resolve(value);
|
|
142
|
+
},
|
|
143
|
+
reject: error => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(error);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
session.subprocess.once('exit', handleExit);
|
|
149
|
+
session.subprocess.send({ ...message, msgid }, err => {
|
|
150
|
+
if (err && this.requests[msgid]) {
|
|
151
|
+
this.requests[msgid].reject(err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
|
|
118
157
|
create_meta: true,
|
|
119
158
|
async create({ conid, database }) {
|
|
120
159
|
const sesid = crypto.randomUUID();
|
|
@@ -202,6 +241,12 @@ module.exports = {
|
|
|
202
241
|
message: 'Query execution started',
|
|
203
242
|
sql,
|
|
204
243
|
});
|
|
244
|
+
let dbinfo = null;
|
|
245
|
+
try {
|
|
246
|
+
dbinfo = (await require('./databaseConnections').ensureOpened(session.conid, session.database))?.structure;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
logger.warn(extractErrorLogData(err), 'DBGM-00000 Error loading structure for query result metadata');
|
|
249
|
+
}
|
|
205
250
|
session.subprocess.send({
|
|
206
251
|
msgtype: 'executeQuery',
|
|
207
252
|
sql,
|
|
@@ -209,6 +254,7 @@ module.exports = {
|
|
|
209
254
|
autoDetectCharts: autoDetectCharts || !!frontMatter?.['selected-chart'],
|
|
210
255
|
limitRows,
|
|
211
256
|
frontMatter,
|
|
257
|
+
dbinfo,
|
|
212
258
|
});
|
|
213
259
|
|
|
214
260
|
return { state: 'ok' };
|
|
@@ -228,6 +274,24 @@ module.exports = {
|
|
|
228
274
|
return { state: 'ok' };
|
|
229
275
|
},
|
|
230
276
|
|
|
277
|
+
saveQueryResultData_meta: true,
|
|
278
|
+
async saveQueryResultData({ sesid, changeSet, sql, autoCommit }, req) {
|
|
279
|
+
await testStandardPermission('dbops/query', req);
|
|
280
|
+
const session = this.opened.find(x => x.sesid == sesid);
|
|
281
|
+
if (!session) {
|
|
282
|
+
throw new Error('Invalid session');
|
|
283
|
+
}
|
|
284
|
+
await testDatabaseRolePermission(session.conid, session.database, 'run_script', req);
|
|
285
|
+
|
|
286
|
+
const res = await this.sendRequest(session, { msgtype: 'saveQueryResultData', changeSet, sql, autoCommit });
|
|
287
|
+
if (res.errorMessage) {
|
|
288
|
+
return {
|
|
289
|
+
errorMessage: res.errorMessage,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return res.result || { state: 'ok' };
|
|
293
|
+
},
|
|
294
|
+
|
|
231
295
|
setIsolationLevel_meta: true,
|
|
232
296
|
async setIsolationLevel({ sesid, level }) {
|
|
233
297
|
const session = this.opened.find(x => x.sesid == sesid);
|
package/src/currentVersion.js
CHANGED
|
@@ -19,12 +19,14 @@ const { handleProcessCommunication } = require('../utility/processComm');
|
|
|
19
19
|
const generateDeploySql = require('../shell/generateDeploySql');
|
|
20
20
|
const { dumpSqlSelect, scriptToSql } = require('dbgate-sqltree');
|
|
21
21
|
const { allowExecuteCustomScript, handleQueryStream } = require('../utility/handleQueryStream');
|
|
22
|
+
const { enrichQueryResultColumns } = require('../utility/queryResultMetadata');
|
|
22
23
|
const dbgateApi = require('../shell');
|
|
23
24
|
const requirePlugin = require('../shell/requirePlugin');
|
|
24
25
|
const path = require('path');
|
|
25
26
|
const { rundir } = require('../utility/directories');
|
|
26
27
|
const fs = require('fs-extra');
|
|
27
28
|
const { changeSetToSql } = require('dbgate-datalib');
|
|
29
|
+
const _ = require('lodash');
|
|
28
30
|
|
|
29
31
|
const logger = getLogger('dbconnProcess');
|
|
30
32
|
|
|
@@ -253,6 +255,9 @@ async function handleQueryData({ msgid, sql, range, commandTimeout }, skipReadon
|
|
|
253
255
|
try {
|
|
254
256
|
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
|
|
255
257
|
const res = await driver.query(dbhan, sql, { range, commandTimeout });
|
|
258
|
+
if (res?.columns) {
|
|
259
|
+
res.columns = await enrichQueryResultColumns({ dbhan, driver, sql, columns: res.columns, dbinfo: analysedStructure });
|
|
260
|
+
}
|
|
256
261
|
process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) });
|
|
257
262
|
} catch (err) {
|
|
258
263
|
process.send({
|
|
@@ -381,6 +386,55 @@ async function handleSaveTableData({ msgid, changeSet }) {
|
|
|
381
386
|
}
|
|
382
387
|
}
|
|
383
388
|
|
|
389
|
+
function validateQueryResultChangeSet(driver, changeSet) {
|
|
390
|
+
if (!driver.databaseEngineTypes?.includes('sql') || !driver.supportsEditableQueryResults) {
|
|
391
|
+
throw new Error('DBGM-00000 Editable query results are not supported by this driver');
|
|
392
|
+
}
|
|
393
|
+
if (changeSet?.inserts?.length > 0 || changeSet?.deletes?.length > 0) {
|
|
394
|
+
throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
|
|
395
|
+
}
|
|
396
|
+
for (const update of changeSet?.updates || []) {
|
|
397
|
+
if (!update.pureName) {
|
|
398
|
+
throw new Error('DBGM-00000 Query result update is missing target table');
|
|
399
|
+
}
|
|
400
|
+
if (_.isEmpty(update.fields)) {
|
|
401
|
+
throw new Error('DBGM-00000 Query result update is missing changed fields');
|
|
402
|
+
}
|
|
403
|
+
if (_.isEmpty(update.condition)) {
|
|
404
|
+
throw new Error('DBGM-00000 Query result update is missing row condition');
|
|
405
|
+
}
|
|
406
|
+
if (Object.values(update.condition).some(value => value === null || value === undefined)) {
|
|
407
|
+
throw new Error('DBGM-00000 Query result update has incomplete row condition');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function handleSaveQueryResultData({ msgid, changeSet, sql }) {
|
|
413
|
+
await waitConnected();
|
|
414
|
+
const driver = requireEngineDriver(storedConnection);
|
|
415
|
+
try {
|
|
416
|
+
ensureExecuteCustomScript(driver);
|
|
417
|
+
validateQueryResultChangeSet(driver, changeSet);
|
|
418
|
+
if (!sql) {
|
|
419
|
+
const script = changeSetToSql({ ...changeSet, inserts: [], deletes: [] }, null, driver.dialect);
|
|
420
|
+
if (script.some(command => command.commandType != 'update')) {
|
|
421
|
+
throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
|
|
422
|
+
}
|
|
423
|
+
sql = scriptToSql(driver, script);
|
|
424
|
+
}
|
|
425
|
+
if (sql) {
|
|
426
|
+
await driver.script(dbhan, sql, { useTransaction: false });
|
|
427
|
+
}
|
|
428
|
+
process.send({ msgtype: 'response', msgid, result: { state: 'ok' } });
|
|
429
|
+
} catch (err) {
|
|
430
|
+
process.send({
|
|
431
|
+
msgtype: 'response',
|
|
432
|
+
msgid,
|
|
433
|
+
errorMessage: extractErrorMessage(err, 'DBGM-00000 Error saving query result data'),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
384
438
|
async function handleMultiCallMethod({ msgid, callList }) {
|
|
385
439
|
try {
|
|
386
440
|
const driver = requireEngineDriver(storedConnection);
|
|
@@ -557,7 +611,7 @@ async function handleExecuteSessionQuery({ sesid, sql }) {
|
|
|
557
611
|
...driver.getQuerySplitterOptions('stream'),
|
|
558
612
|
returnRichInfo: true,
|
|
559
613
|
})) {
|
|
560
|
-
await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid);
|
|
614
|
+
await handleQueryStream(dbhan, driver, queryStreamInfoHolder, sqlItem, sesid, undefined, undefined, false, analysedStructure);
|
|
561
615
|
if (queryStreamInfoHolder.canceled) {
|
|
562
616
|
break;
|
|
563
617
|
}
|
|
@@ -599,6 +653,7 @@ const messageHandlers = {
|
|
|
599
653
|
runOperation: handleRunOperation,
|
|
600
654
|
updateCollection: handleUpdateCollection,
|
|
601
655
|
saveTableData: handleSaveTableData,
|
|
656
|
+
saveQueryResultData: handleSaveQueryResultData,
|
|
602
657
|
collectionData: handleCollectionData,
|
|
603
658
|
loadKeys: handleLoadKeys,
|
|
604
659
|
scanKeys: handleScanKeys,
|
|
@@ -10,7 +10,9 @@ const requireEngineDriver = require('../utility/requireEngineDriver');
|
|
|
10
10
|
const { decryptConnection } = require('../utility/crypting');
|
|
11
11
|
const { connectUtility } = require('../utility/connectUtility');
|
|
12
12
|
const { handleProcessCommunication } = require('../utility/processComm');
|
|
13
|
-
const { getLogger, extractIntSettingsValue, extractBoolSettingsValue } = require('dbgate-tools');
|
|
13
|
+
const { getLogger, extractIntSettingsValue, extractBoolSettingsValue, extractErrorMessage } = require('dbgate-tools');
|
|
14
|
+
const { changeSetToSql } = require('dbgate-datalib');
|
|
15
|
+
const { scriptToSql } = require('dbgate-sqltree');
|
|
14
16
|
const { handleQueryStream, QueryStreamTableWriter, allowExecuteCustomScript } = require('../utility/handleQueryStream');
|
|
15
17
|
|
|
16
18
|
const logger = getLogger('sessionProcess');
|
|
@@ -119,7 +121,7 @@ async function handleExecuteControlCommand({ command }) {
|
|
|
119
121
|
process.send({
|
|
120
122
|
msgtype: 'info',
|
|
121
123
|
info: {
|
|
122
|
-
message: 'Connection
|
|
124
|
+
message: 'Connection is read-only',
|
|
123
125
|
severity: 'error',
|
|
124
126
|
},
|
|
125
127
|
});
|
|
@@ -149,7 +151,7 @@ async function handleExecuteControlCommand({ command }) {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows, frontMatter }) {
|
|
154
|
+
async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows, frontMatter, dbinfo }) {
|
|
153
155
|
lastActivity = new Date().getTime();
|
|
154
156
|
|
|
155
157
|
await waitConnected();
|
|
@@ -159,7 +161,7 @@ async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows
|
|
|
159
161
|
process.send({
|
|
160
162
|
msgtype: 'info',
|
|
161
163
|
info: {
|
|
162
|
-
message: 'Connection
|
|
164
|
+
message: 'Connection is read-only',
|
|
163
165
|
severity: 'error',
|
|
164
166
|
},
|
|
165
167
|
});
|
|
@@ -186,7 +188,8 @@ async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows
|
|
|
186
188
|
undefined,
|
|
187
189
|
limitRows,
|
|
188
190
|
frontMatter,
|
|
189
|
-
autoDetectCharts
|
|
191
|
+
autoDetectCharts,
|
|
192
|
+
dbinfo
|
|
190
193
|
);
|
|
191
194
|
// const handler = new StreamHandler(resultIndex);
|
|
192
195
|
// const stream = await driver.stream(systemConnection, sqlItem, handler);
|
|
@@ -203,6 +206,69 @@ async function handleExecuteQuery({ sql, autoCommit, autoDetectCharts, limitRows
|
|
|
203
206
|
}
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
function validateQueryResultChangeSet(driver, changeSet) {
|
|
210
|
+
if (!driver.databaseEngineTypes?.includes('sql') || !driver.supportsEditableQueryResults) {
|
|
211
|
+
throw new Error('DBGM-00000 Editable query results are not supported by this driver');
|
|
212
|
+
}
|
|
213
|
+
if (changeSet?.inserts?.length > 0 || changeSet?.deletes?.length > 0) {
|
|
214
|
+
throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
|
|
215
|
+
}
|
|
216
|
+
for (const update of changeSet?.updates || []) {
|
|
217
|
+
if (!update.pureName) {
|
|
218
|
+
throw new Error('DBGM-00000 Query result update is missing target table');
|
|
219
|
+
}
|
|
220
|
+
if (_.isEmpty(update.fields)) {
|
|
221
|
+
throw new Error('DBGM-00000 Query result update is missing changed fields');
|
|
222
|
+
}
|
|
223
|
+
if (_.isEmpty(update.condition)) {
|
|
224
|
+
throw new Error('DBGM-00000 Query result update is missing row condition');
|
|
225
|
+
}
|
|
226
|
+
if (Object.values(update.condition).some(value => value === null || value === undefined)) {
|
|
227
|
+
throw new Error('DBGM-00000 Query result update has incomplete row condition');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handleSaveQueryResultData({ msgid, changeSet, sql, autoCommit }) {
|
|
233
|
+
lastActivity = new Date().getTime();
|
|
234
|
+
|
|
235
|
+
await waitConnected();
|
|
236
|
+
const driver = requireEngineDriver(storedConnection);
|
|
237
|
+
try {
|
|
238
|
+
if (!allowExecuteCustomScript(storedConnection, driver)) {
|
|
239
|
+
throw new Error('DBGM-00000 Connection is read-only');
|
|
240
|
+
}
|
|
241
|
+
validateQueryResultChangeSet(driver, changeSet);
|
|
242
|
+
if (!sql) {
|
|
243
|
+
const script = changeSetToSql({ ...changeSet, inserts: [], deletes: [] }, null, driver.dialect);
|
|
244
|
+
if (script.some(command => command.commandType != 'update')) {
|
|
245
|
+
throw new Error('DBGM-00000 Query result saving supports UPDATE operations only');
|
|
246
|
+
}
|
|
247
|
+
sql = scriptToSql(driver, script);
|
|
248
|
+
}
|
|
249
|
+
if (sql) {
|
|
250
|
+
executingScripts++;
|
|
251
|
+
try {
|
|
252
|
+
await driver.script(dbhan, sql, { useTransaction: false });
|
|
253
|
+
if (autoCommit) {
|
|
254
|
+
const dmp = driver.createDumper();
|
|
255
|
+
dmp.commitTransaction();
|
|
256
|
+
await driver.query(dbhan, dmp.s, { discardResult: true });
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
executingScripts--;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
process.send({ msgtype: 'response', msgid, result: { state: 'ok' } });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
process.send({
|
|
265
|
+
msgtype: 'response',
|
|
266
|
+
msgid,
|
|
267
|
+
errorMessage: extractErrorMessage(err, 'DBGM-00000 Error saving query result data'),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
206
272
|
async function handleExecuteReader({ jslid, sql, fileName }) {
|
|
207
273
|
lastActivity = new Date().getTime();
|
|
208
274
|
|
|
@@ -244,6 +310,7 @@ const messageHandlers = {
|
|
|
244
310
|
executeControlCommand: handleExecuteControlCommand,
|
|
245
311
|
setIsolationLevel: handleSetIsolationLevel,
|
|
246
312
|
executeReader: handleExecuteReader,
|
|
313
|
+
saveQueryResultData: handleSaveQueryResultData,
|
|
247
314
|
startProfiler: handleStartProfiler,
|
|
248
315
|
stopProfiler: handleStopProfiler,
|
|
249
316
|
ping: handlePing,
|
|
@@ -4,9 +4,12 @@ const fs = require('fs');
|
|
|
4
4
|
const _ = require('lodash');
|
|
5
5
|
|
|
6
6
|
const { jsldir } = require('../utility/directories');
|
|
7
|
-
const { serializeJsTypesReplacer } = require('dbgate-tools');
|
|
7
|
+
const { serializeJsTypesReplacer, getLogger } = require('dbgate-tools');
|
|
8
8
|
const { ChartProcessor } = require('dbgate-datalib');
|
|
9
9
|
const { isProApp } = require('./checkLicense');
|
|
10
|
+
const { enrichQueryResultColumns } = require('./queryResultMetadata');
|
|
11
|
+
|
|
12
|
+
const logger = getLogger('handleQueryStream');
|
|
10
13
|
|
|
11
14
|
class QueryStreamTableWriter {
|
|
12
15
|
constructor(sesid = undefined) {
|
|
@@ -144,13 +147,21 @@ class StreamHandler {
|
|
|
144
147
|
sesid = undefined,
|
|
145
148
|
limitRows = undefined,
|
|
146
149
|
frontMatter = undefined,
|
|
147
|
-
autoDetectCharts = false
|
|
150
|
+
autoDetectCharts = false,
|
|
151
|
+
supportsEditableQueryResults = false,
|
|
152
|
+
dbhan = null,
|
|
153
|
+
driver = null,
|
|
154
|
+
dbinfo = null
|
|
148
155
|
) {
|
|
149
156
|
this.recordset = this.recordset.bind(this);
|
|
150
157
|
this.startLine = startLine;
|
|
151
158
|
this.sesid = sesid;
|
|
152
159
|
this.frontMatter = frontMatter;
|
|
153
160
|
this.autoDetectCharts = autoDetectCharts;
|
|
161
|
+
this.supportsEditableQueryResults = supportsEditableQueryResults;
|
|
162
|
+
this.dbhan = dbhan;
|
|
163
|
+
this.driver = driver;
|
|
164
|
+
this.dbinfo = dbinfo;
|
|
154
165
|
this.limitRows = limitRows;
|
|
155
166
|
this.rowsLimitOverflow = false;
|
|
156
167
|
this.row = this.row.bind(this);
|
|
@@ -165,7 +176,8 @@ class StreamHandler {
|
|
|
165
176
|
this.plannedStats = false;
|
|
166
177
|
this.queryStreamInfoHolder = queryStreamInfoHolder;
|
|
167
178
|
this.resolve = resolve;
|
|
168
|
-
this.
|
|
179
|
+
this.currentRecordset = null;
|
|
180
|
+
this.recordsetQueuePromise = Promise.resolve();
|
|
169
181
|
// currentHandlers = [...currentHandlers, this];
|
|
170
182
|
}
|
|
171
183
|
|
|
@@ -180,21 +192,37 @@ class StreamHandler {
|
|
|
180
192
|
process.send({ msgtype: 'changedCurrentDatabase', database, sesid: this.sesid });
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
|
|
195
|
+
async prepareRecordset(recordsetContext, columns, options) {
|
|
184
196
|
if (this.rowsLimitOverflow) {
|
|
185
197
|
return;
|
|
186
198
|
}
|
|
187
199
|
this.closeCurrentWriter();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.
|
|
200
|
+
const writer = new QueryStreamTableWriter(this.sesid);
|
|
201
|
+
const structure = Array.isArray(columns) ? { columns } : columns;
|
|
202
|
+
const enrichedColumns = await enrichQueryStreamColumns(
|
|
203
|
+
structure?.columns,
|
|
204
|
+
this.sql,
|
|
205
|
+
this.supportsEditableQueryResults,
|
|
206
|
+
this.driver,
|
|
207
|
+
this.dbhan,
|
|
208
|
+
this.dbinfo
|
|
209
|
+
);
|
|
210
|
+
writer.initializeFromQuery(
|
|
211
|
+
{
|
|
212
|
+
...structure,
|
|
213
|
+
columns: enrichedColumns,
|
|
214
|
+
},
|
|
215
|
+
recordsetContext.resultIndex,
|
|
216
|
+
this.frontMatter?.[`chart-${recordsetContext.resultIndex + 1}`],
|
|
193
217
|
this.autoDetectCharts,
|
|
194
218
|
options
|
|
195
219
|
);
|
|
196
|
-
|
|
197
|
-
this.
|
|
220
|
+
recordsetContext.writer = writer;
|
|
221
|
+
this.currentWriter = writer;
|
|
222
|
+
for (const row of recordsetContext.pendingRows) {
|
|
223
|
+
this.writeRow(recordsetContext, row, false);
|
|
224
|
+
}
|
|
225
|
+
recordsetContext.pendingRows = [];
|
|
198
226
|
|
|
199
227
|
// this.writeCurrentStats();
|
|
200
228
|
|
|
@@ -204,15 +232,50 @@ class StreamHandler {
|
|
|
204
232
|
// }
|
|
205
233
|
// }, 500);
|
|
206
234
|
}
|
|
235
|
+
|
|
236
|
+
recordset(columns, options) {
|
|
237
|
+
const recordsetContext = {
|
|
238
|
+
pendingRows: [],
|
|
239
|
+
resultIndex: this.queryStreamInfoHolder.resultIndex,
|
|
240
|
+
rowCounter: 0,
|
|
241
|
+
writer: null,
|
|
242
|
+
};
|
|
243
|
+
this.queryStreamInfoHolder.resultIndex += 1;
|
|
244
|
+
this.currentRecordset = recordsetContext;
|
|
245
|
+
|
|
246
|
+
const recordsetReadyPromise = this.recordsetQueuePromise
|
|
247
|
+
.then(() => this.prepareRecordset(recordsetContext, columns, options))
|
|
248
|
+
.catch(err => {
|
|
249
|
+
recordsetContext.pendingRows = [];
|
|
250
|
+
this.info({
|
|
251
|
+
message: err?.message || `${err}`,
|
|
252
|
+
severity: 'error',
|
|
253
|
+
});
|
|
254
|
+
})
|
|
255
|
+
.finally(() => {
|
|
256
|
+
if (this.recordsetReadyPromise == recordsetReadyPromise) {
|
|
257
|
+
this.recordsetReadyPromise = null;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
this.recordsetReadyPromise = recordsetReadyPromise;
|
|
261
|
+
this.recordsetQueuePromise = recordsetReadyPromise;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
writeRow(recordsetContext, row, incrementCounter = true) {
|
|
265
|
+
recordsetContext.writer.row(row);
|
|
266
|
+
if (incrementCounter) recordsetContext.rowCounter += 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
207
269
|
row(row) {
|
|
208
270
|
if (this.rowsLimitOverflow) {
|
|
209
271
|
return;
|
|
210
272
|
}
|
|
211
273
|
|
|
212
|
-
|
|
274
|
+
const recordsetContext = this.currentRecordset;
|
|
275
|
+
if (this.limitRows && recordsetContext?.rowCounter >= this.limitRows) {
|
|
213
276
|
process.send({
|
|
214
277
|
msgtype: 'info',
|
|
215
|
-
info: { message: `Rows limit overflow, loaded ${
|
|
278
|
+
info: { message: `Rows limit overflow, loaded ${recordsetContext.rowCounter} rows, canceling query`, severity: 'error' },
|
|
216
279
|
sesid: this.sesid,
|
|
217
280
|
});
|
|
218
281
|
this.rowsLimitOverflow = true;
|
|
@@ -229,9 +292,11 @@ class StreamHandler {
|
|
|
229
292
|
return;
|
|
230
293
|
}
|
|
231
294
|
|
|
232
|
-
if (
|
|
233
|
-
this.
|
|
234
|
-
|
|
295
|
+
if (recordsetContext?.writer) {
|
|
296
|
+
this.writeRow(recordsetContext, row);
|
|
297
|
+
} else if (recordsetContext) {
|
|
298
|
+
recordsetContext.pendingRows.push(row);
|
|
299
|
+
recordsetContext.rowCounter += 1;
|
|
235
300
|
} else if (row.message) {
|
|
236
301
|
process.send({ msgtype: 'info', info: { message: row.message }, sesid: this.sesid });
|
|
237
302
|
}
|
|
@@ -241,9 +306,12 @@ class StreamHandler {
|
|
|
241
306
|
// process.send({ msgtype: 'error', error });
|
|
242
307
|
// }
|
|
243
308
|
done(result) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
309
|
+
const finish = () => {
|
|
310
|
+
this.closeCurrentWriter();
|
|
311
|
+
// currentHandlers = currentHandlers.filter((x) => x != this);
|
|
312
|
+
this.resolve();
|
|
313
|
+
};
|
|
314
|
+
this.recordsetQueuePromise.then(finish);
|
|
247
315
|
}
|
|
248
316
|
info(info) {
|
|
249
317
|
if (info && info.line != null) {
|
|
@@ -259,6 +327,19 @@ class StreamHandler {
|
|
|
259
327
|
}
|
|
260
328
|
}
|
|
261
329
|
|
|
330
|
+
async function enrichQueryStreamColumns(columns, sql, supportsEditableQueryResults, driver, dbhan, dbinfo) {
|
|
331
|
+
if (!supportsEditableQueryResults) return columns;
|
|
332
|
+
return enrichQueryResultColumns({
|
|
333
|
+
columns,
|
|
334
|
+
sql,
|
|
335
|
+
driver,
|
|
336
|
+
dbhan,
|
|
337
|
+
dbinfo,
|
|
338
|
+
onNativeMetadataError: err => logger.warn('Error enriching query stream columns', err),
|
|
339
|
+
onFallbackMetadataError: err => logger.warn('Error parsing query stream column metadata', err),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
262
343
|
function handleQueryStream(
|
|
263
344
|
dbhan,
|
|
264
345
|
driver,
|
|
@@ -267,7 +348,8 @@ function handleQueryStream(
|
|
|
267
348
|
sesid = undefined,
|
|
268
349
|
limitRows = undefined,
|
|
269
350
|
frontMatter = undefined,
|
|
270
|
-
autoDetectCharts = false
|
|
351
|
+
autoDetectCharts = false,
|
|
352
|
+
dbinfo = null
|
|
271
353
|
) {
|
|
272
354
|
return new Promise((resolve, reject) => {
|
|
273
355
|
const start = sqlItem.trimStart || sqlItem.start;
|
|
@@ -278,8 +360,13 @@ function handleQueryStream(
|
|
|
278
360
|
sesid,
|
|
279
361
|
limitRows,
|
|
280
362
|
frontMatter,
|
|
281
|
-
autoDetectCharts
|
|
363
|
+
autoDetectCharts,
|
|
364
|
+
driver.databaseEngineTypes?.includes('sql') && driver.supportsEditableQueryResults,
|
|
365
|
+
dbhan,
|
|
366
|
+
driver,
|
|
367
|
+
dbinfo
|
|
282
368
|
);
|
|
369
|
+
handler.sql = sqlItem.text;
|
|
283
370
|
driver.stream(dbhan, sqlItem.text, handler);
|
|
284
371
|
});
|
|
285
372
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const {
|
|
2
|
+
extractSingleTableFromSql,
|
|
3
|
+
extractColumnSourcesFromSql,
|
|
4
|
+
extractColumnMetadataFromSql,
|
|
5
|
+
} = require('dbgate-tools');
|
|
6
|
+
|
|
7
|
+
function getColumnSourceName(columnSources, columnName) {
|
|
8
|
+
if (!columnSources || !columnName) return null;
|
|
9
|
+
if (columnSources[columnName]) return columnSources[columnName];
|
|
10
|
+
|
|
11
|
+
const matchingKeys = Object.keys(columnSources).filter(key => key.toLowerCase() == columnName.toLowerCase());
|
|
12
|
+
return matchingKeys.length == 1 ? columnSources[matchingKeys[0]] : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getColumnMetadata(columnMetadata, columnName) {
|
|
16
|
+
if (!columnMetadata || !columnName) return null;
|
|
17
|
+
if (columnMetadata[columnName]) return columnMetadata[columnName];
|
|
18
|
+
|
|
19
|
+
const matchingKeys = Object.keys(columnMetadata).filter(key => key.toLowerCase() == columnName.toLowerCase());
|
|
20
|
+
return matchingKeys.length == 1 ? columnMetadata[matchingKeys[0]] : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, onNativeMetadataError, onFallbackMetadataError }) {
|
|
24
|
+
if (!columns?.length || !driver?.databaseEngineTypes?.includes('sql') || !driver?.supportsEditableQueryResults) {
|
|
25
|
+
return columns;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (driver.enrichColumnMetadata) {
|
|
29
|
+
try {
|
|
30
|
+
const enriched = await driver.enrichColumnMetadata(dbhan, sql, columns, dbinfo);
|
|
31
|
+
if (enriched?.every(column => column.tableName && column.sourceColumnName)) return enriched;
|
|
32
|
+
if (enriched?.length == columns.length) columns = enriched;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
onNativeMetadataError?.(err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const columnMetadata = extractColumnMetadataFromSql(sql);
|
|
40
|
+
if (columnMetadata) {
|
|
41
|
+
return columns.map(column => {
|
|
42
|
+
const metadata = getColumnMetadata(columnMetadata, column.columnName);
|
|
43
|
+
return {
|
|
44
|
+
...column,
|
|
45
|
+
tableName: column.tableName || metadata?.tableName,
|
|
46
|
+
tableSchema: column.tableSchema || metadata?.schemaName,
|
|
47
|
+
sourceColumnName: column.sourceColumnName || metadata?.sourceColumnName,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const table = extractSingleTableFromSql(sql);
|
|
53
|
+
if (!table) return columns;
|
|
54
|
+
const columnSources = extractColumnSourcesFromSql(sql);
|
|
55
|
+
|
|
56
|
+
return columns.map(column => ({
|
|
57
|
+
...column,
|
|
58
|
+
tableName: column.tableName || table.tableName,
|
|
59
|
+
tableSchema: column.tableSchema || table.schemaName,
|
|
60
|
+
sourceColumnName: column.sourceColumnName || getColumnSourceName(columnSources, column.columnName) || column.columnName,
|
|
61
|
+
}));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
onFallbackMetadataError?.(err);
|
|
64
|
+
return columns;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
enrichQueryResultColumns,
|
|
70
|
+
};
|