dbgate-api 7.1.13 → 7.2.1
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/config.js +17 -0
- package/src/controllers/databaseConnections.js +24 -2
- package/src/controllers/sessions.js +64 -0
- package/src/currentVersion.js +2 -2
- package/src/proc/databaseConnectionProcess.js +96 -4
- package/src/proc/sessionProcess.js +92 -8
- package/src/shell/copyStream.js +2 -1
- package/src/utility/handleQueryStream.js +108 -21
- package/src/utility/healthStatus.js +3 -1
- package/src/utility/loginchecker.js +5 -0
- package/src/utility/queryResultMetadata.js +333 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbgate-api",
|
|
3
3
|
"main": "src/index.js",
|
|
4
|
-
"version": "7.1
|
|
4
|
+
"version": "7.2.1",
|
|
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.1
|
|
33
|
+
"dbgate-datalib": "7.2.1",
|
|
34
34
|
"dbgate-query-splitter": "^4.12.0",
|
|
35
|
-
"dbgate-rest": "7.1
|
|
36
|
-
"dbgate-sqltree": "7.1
|
|
37
|
-
"dbgate-tools": "7.1
|
|
35
|
+
"dbgate-rest": "7.2.1",
|
|
36
|
+
"dbgate-sqltree": "7.2.1",
|
|
37
|
+
"dbgate-tools": "7.2.1",
|
|
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.1
|
|
99
|
+
"dbgate-types": "7.2.1",
|
|
100
100
|
"env-cmd": "^10.1.0",
|
|
101
101
|
"jest": "^30.4.2",
|
|
102
102
|
"jsdoc-to-markdown": "^9.0.5",
|
|
@@ -16,6 +16,7 @@ const connections = require('../controllers/connections');
|
|
|
16
16
|
const { getAuthProviderFromReq } = require('../auth/authProvider');
|
|
17
17
|
const { checkLicense, checkLicenseKey } = require('../utility/checkLicense');
|
|
18
18
|
const storage = require('./storage');
|
|
19
|
+
const dbgateApi = require('../shell');
|
|
19
20
|
const { getAuthProxyUrl, tryToGetRefreshedLicense } = require('../utility/authProxy');
|
|
20
21
|
const { getPublicHardwareFingerprint } = require('../utility/hardwareFingerprint');
|
|
21
22
|
const { extractErrorMessage } = require('dbgate-tools');
|
|
@@ -438,4 +439,20 @@ module.exports = {
|
|
|
438
439
|
|
|
439
440
|
return true;
|
|
440
441
|
},
|
|
442
|
+
|
|
443
|
+
createConnectionsAndSettingsZip_meta: true,
|
|
444
|
+
async createConnectionsAndSettingsZip({ db, filePath }, req) {
|
|
445
|
+
const loadedPermissions = await loadPermissionsFromRequest(req);
|
|
446
|
+
if (!hasPermission(`admin/config`, loadedPermissions)) {
|
|
447
|
+
throw new Error('Permission denied: admin/config');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (connections.portalConnections) {
|
|
451
|
+
throw new Error('Not allowed');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const exportDb = process.env.STORAGE_DATABASE ? await storage.fillTeamFileContentForExport(db) : db;
|
|
455
|
+
await dbgateApi.zipJsonLinesData(exportDb, filePath);
|
|
456
|
+
return true;
|
|
457
|
+
},
|
|
441
458
|
};
|
|
@@ -157,8 +157,15 @@ module.exports = {
|
|
|
157
157
|
|
|
158
158
|
handle_copyStreamError(conid, database, { copyStreamError }) {
|
|
159
159
|
const { progressName } = copyStreamError;
|
|
160
|
-
const
|
|
161
|
-
logger.error(
|
|
160
|
+
const runid = progressName?.runid;
|
|
161
|
+
logger.error({ conid, database, copyStreamError }, 'DBGM-00000 Error in database connection copy stream');
|
|
162
|
+
if (!runid) return;
|
|
163
|
+
if (copyStreamError.dbgateCopyStreamErrorReported) return;
|
|
164
|
+
socket.emit(`runner-progress-${runid}`, {
|
|
165
|
+
progressName: progressName?.name,
|
|
166
|
+
status: 'error',
|
|
167
|
+
errorMessage: copyStreamError.message,
|
|
168
|
+
});
|
|
162
169
|
socket.emit(`runner-done-${runid}`);
|
|
163
170
|
},
|
|
164
171
|
|
|
@@ -517,6 +524,21 @@ module.exports = {
|
|
|
517
524
|
return res.result || null;
|
|
518
525
|
},
|
|
519
526
|
|
|
527
|
+
saveQueryResultData_meta: true,
|
|
528
|
+
async saveQueryResultData({ conid, database, changeSet, sql }, req) {
|
|
529
|
+
await testConnectionPermission(conid, req);
|
|
530
|
+
await testDatabaseRolePermission(conid, database, 'run_script', req);
|
|
531
|
+
|
|
532
|
+
const opened = await this.ensureOpened(conid, database);
|
|
533
|
+
const res = await this.sendRequest(opened, { msgtype: 'saveQueryResultData', changeSet, sql });
|
|
534
|
+
if (res.errorMessage) {
|
|
535
|
+
return {
|
|
536
|
+
errorMessage: res.errorMessage,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return res.result || { state: 'ok' };
|
|
540
|
+
},
|
|
541
|
+
|
|
520
542
|
multiCallMethod_meta: true,
|
|
521
543
|
async multiCallMethod({ conid, database, callList }, req) {
|
|
522
544
|
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
|
}
|
|
@@ -569,13 +623,50 @@ async function handleEvalJsonScript({ script, runid }) {
|
|
|
569
623
|
const directory = path.join(rundir(), runid);
|
|
570
624
|
fs.mkdirSync(directory);
|
|
571
625
|
const originalCwd = process.cwd();
|
|
626
|
+
let scriptError = null;
|
|
627
|
+
let finalizerError = null;
|
|
572
628
|
|
|
573
629
|
try {
|
|
574
630
|
process.chdir(directory);
|
|
575
631
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
632
|
+
try {
|
|
633
|
+
const evalWriter = new ScriptWriterEval(dbgateApi, requirePlugin, dbhan, runid);
|
|
634
|
+
await playJsonScriptWriter(script, evalWriter);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
scriptError = err;
|
|
637
|
+
} finally {
|
|
638
|
+
try {
|
|
639
|
+
await dbgateApi.finalizer.run();
|
|
640
|
+
} catch (err) {
|
|
641
|
+
finalizerError = err;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const shouldReportScriptError = scriptError && !scriptError.dbgateCopyStreamErrorReported;
|
|
646
|
+
|
|
647
|
+
if (shouldReportScriptError || finalizerError) {
|
|
648
|
+
if (shouldReportScriptError) {
|
|
649
|
+
logger.error(extractErrorLogData(scriptError), 'DBGM-00000 Error running JSON script on database connection');
|
|
650
|
+
}
|
|
651
|
+
if (finalizerError) {
|
|
652
|
+
logger.error(extractErrorLogData(finalizerError), 'DBGM-00000 Error running JSON script finalizers');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
process.send({
|
|
656
|
+
msgtype: 'copyStreamError',
|
|
657
|
+
copyStreamError: {
|
|
658
|
+
message: [
|
|
659
|
+
shouldReportScriptError && extractErrorMessage(scriptError),
|
|
660
|
+
finalizerError && `Finalizer failed: ${extractErrorMessage(finalizerError)}`,
|
|
661
|
+
]
|
|
662
|
+
.filter(Boolean)
|
|
663
|
+
.join('\n'),
|
|
664
|
+
progressName: { name: 'script', runid },
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
} else {
|
|
668
|
+
process.send({ msgtype: 'runnerDone', runid });
|
|
669
|
+
}
|
|
579
670
|
} finally {
|
|
580
671
|
process.chdir(originalCwd);
|
|
581
672
|
}
|
|
@@ -599,6 +690,7 @@ const messageHandlers = {
|
|
|
599
690
|
runOperation: handleRunOperation,
|
|
600
691
|
updateCollection: handleUpdateCollection,
|
|
601
692
|
saveTableData: handleSaveTableData,
|
|
693
|
+
saveQueryResultData: handleSaveQueryResultData,
|
|
602
694
|
collectionData: handleCollectionData,
|
|
603
695
|
loadKeys: handleLoadKeys,
|
|
604
696
|
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
|
|
|
@@ -224,14 +290,31 @@ async function handleExecuteReader({ jslid, sql, fileName }) {
|
|
|
224
290
|
|
|
225
291
|
const reader = await driver.readQuery(dbhan, sql);
|
|
226
292
|
|
|
293
|
+
let isFinished = false;
|
|
294
|
+
const finishReader = () => {
|
|
295
|
+
if (isFinished) return;
|
|
296
|
+
isFinished = true;
|
|
297
|
+
writer.close().then(() => {
|
|
298
|
+
process.send({ msgtype: 'done' });
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
227
302
|
reader.on('data', data => {
|
|
303
|
+
if (isFinished) return;
|
|
228
304
|
writer.rowFromReader(data);
|
|
229
305
|
});
|
|
230
|
-
reader.on('
|
|
231
|
-
|
|
232
|
-
|
|
306
|
+
reader.on('error', err => {
|
|
307
|
+
process.send({
|
|
308
|
+
msgtype: 'info',
|
|
309
|
+
info: {
|
|
310
|
+
message: extractErrorMessage(err),
|
|
311
|
+
severity: 'error',
|
|
312
|
+
time: new Date(),
|
|
313
|
+
},
|
|
233
314
|
});
|
|
315
|
+
finishReader();
|
|
234
316
|
});
|
|
317
|
+
reader.on('end', finishReader);
|
|
235
318
|
}
|
|
236
319
|
|
|
237
320
|
function handlePing() {
|
|
@@ -244,6 +327,7 @@ const messageHandlers = {
|
|
|
244
327
|
executeControlCommand: handleExecuteControlCommand,
|
|
245
328
|
setIsolationLevel: handleSetIsolationLevel,
|
|
246
329
|
executeReader: handleExecuteReader,
|
|
330
|
+
saveQueryResultData: handleSaveQueryResultData,
|
|
247
331
|
startProfiler: handleStartProfiler,
|
|
248
332
|
stopProfiler: handleStopProfiler,
|
|
249
333
|
ping: handlePing,
|
package/src/shell/copyStream.js
CHANGED
|
@@ -66,6 +66,7 @@ async function copyStream(input, output, options) {
|
|
|
66
66
|
}
|
|
67
67
|
} catch (err) {
|
|
68
68
|
logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed');
|
|
69
|
+
err.dbgateCopyStreamErrorReported = true;
|
|
69
70
|
|
|
70
71
|
process.send({
|
|
71
72
|
msgtype: 'copyStreamError',
|
|
@@ -84,7 +85,7 @@ async function copyStream(input, output, options) {
|
|
|
84
85
|
errorMessage: extractErrorMessage(err),
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
throw err;
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -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
|
}
|
|
@@ -4,6 +4,7 @@ const databaseConnections = require('../controllers/databaseConnections');
|
|
|
4
4
|
const serverConnections = require('../controllers/serverConnections');
|
|
5
5
|
const sessions = require('../controllers/sessions');
|
|
6
6
|
const runners = require('../controllers/runners');
|
|
7
|
+
const { getLoggedUserCount } = require('./loginchecker');
|
|
7
8
|
|
|
8
9
|
async function getHealthStatus() {
|
|
9
10
|
const memory = process.memoryUsage();
|
|
@@ -13,7 +14,8 @@ async function getHealthStatus() {
|
|
|
13
14
|
status: 'ok',
|
|
14
15
|
databaseConnectionCount: databaseConnections.opened.length,
|
|
15
16
|
serverConnectionCount: serverConnections.opened.length,
|
|
16
|
-
|
|
17
|
+
querySessionCount: sessions.opened.length,
|
|
18
|
+
loggedUserCount: getLoggedUserCount(),
|
|
17
19
|
runProcessCount: runners.opened.length,
|
|
18
20
|
memory,
|
|
19
21
|
cpuUsage,
|
|
@@ -10,9 +10,14 @@ function markLoginAsLoggedOut(licenseUid) {}
|
|
|
10
10
|
|
|
11
11
|
const LOGIN_LIMIT_ERROR = '';
|
|
12
12
|
|
|
13
|
+
function getLoggedUserCount() {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
module.exports = {
|
|
14
18
|
markUserAsActive,
|
|
15
19
|
isLoginLicensed,
|
|
16
20
|
markLoginAsLoggedOut,
|
|
21
|
+
getLoggedUserCount,
|
|
17
22
|
LOGIN_LIMIT_ERROR,
|
|
18
23
|
};
|
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
function namesEqual(left, right) {
|
|
24
|
+
return left != null && right != null && left.toLowerCase() == right.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findTable(dbinfo, schemaName, pureName) {
|
|
28
|
+
if (!dbinfo?.tables?.length || !pureName) return null;
|
|
29
|
+
const schemaMatches = table => !schemaName || namesEqual(table.schemaName, schemaName);
|
|
30
|
+
const exactTables = dbinfo.tables.filter(table => schemaMatches(table) && table.pureName == pureName);
|
|
31
|
+
if (exactTables.length == 1) return exactTables[0];
|
|
32
|
+
const matchingTables = dbinfo.tables.filter(table => schemaMatches(table) && namesEqual(table.pureName, pureName));
|
|
33
|
+
if (matchingTables.length == 1) return matchingTables[0];
|
|
34
|
+
const uniqueNameTables = dbinfo.tables.filter(table => namesEqual(table.pureName, pureName));
|
|
35
|
+
return uniqueNameTables.length == 1 ? uniqueNameTables[0] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findView(dbinfo, schemaName, pureName) {
|
|
39
|
+
const views = [...(dbinfo?.views || []), ...(dbinfo?.matviews || [])];
|
|
40
|
+
if (!views.length || !pureName) return null;
|
|
41
|
+
if (schemaName) {
|
|
42
|
+
const exactView = views.find(view => view.schemaName == schemaName && view.pureName == pureName);
|
|
43
|
+
if (exactView) return exactView;
|
|
44
|
+
const schemaViews = views.filter(view => namesEqual(view.schemaName, schemaName) && namesEqual(view.pureName, pureName));
|
|
45
|
+
if (schemaViews.length == 1) return schemaViews[0];
|
|
46
|
+
const schemaLessViews = views.filter(view => !view.schemaName && namesEqual(view.pureName, pureName));
|
|
47
|
+
return schemaLessViews.length == 1 ? schemaLessViews[0] : null;
|
|
48
|
+
}
|
|
49
|
+
const matchingViews = views.filter(view => namesEqual(view.pureName, pureName));
|
|
50
|
+
return matchingViews.length == 1 ? matchingViews[0] : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractSelectFromCreateView(createSql) {
|
|
54
|
+
if (!createSql) return null;
|
|
55
|
+
const match = createSql.match(/\bas\s+(select\b[\s\S]*)/i);
|
|
56
|
+
return match ? match[1] : createSql;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unquoteIdentifier(identifier) {
|
|
60
|
+
const trimmed = identifier?.trim();
|
|
61
|
+
if (!trimmed) return null;
|
|
62
|
+
if (
|
|
63
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
64
|
+
(trimmed.startsWith('`') && trimmed.endsWith('`')) ||
|
|
65
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
66
|
+
) {
|
|
67
|
+
return trimmed.slice(1, -1).replace(/""/g, '"').replace(/``/g, '`').replace(/]]/g, ']');
|
|
68
|
+
}
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractCreateViewColumnNames(createSql) {
|
|
73
|
+
if (!createSql) return null;
|
|
74
|
+
const asMatch = createSql.match(/\bas\b/i);
|
|
75
|
+
if (!asMatch) return null;
|
|
76
|
+
const prefix = createSql.substring(0, asMatch.index);
|
|
77
|
+
const match = prefix.match(/\(([^()]*)\)\s*$/);
|
|
78
|
+
if (!match) return null;
|
|
79
|
+
const columns = match[1]
|
|
80
|
+
.split(',')
|
|
81
|
+
.map(column => unquoteIdentifier(column))
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
return columns.length > 0 ? columns : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getViewColumnNames(view) {
|
|
87
|
+
const analysedColumns = view?.columns?.map(column => column.columnName).filter(Boolean);
|
|
88
|
+
if (analysedColumns?.length) return analysedColumns;
|
|
89
|
+
return extractCreateViewColumnNames(view?.createSql);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isSelectStarFromSingleTable(selectSql) {
|
|
93
|
+
return /^\s*select\s+\*\s+from\b/i.test(selectSql || '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getSelectListItemCount(selectSql) {
|
|
97
|
+
const selectMatch = selectSql?.match(/^\s*select\s+([\s\S]+?)\s+from\s/i);
|
|
98
|
+
return selectMatch ? splitTopLevelCommaList(selectMatch[1]).length : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const identifierPattern = '`(?:``|[^`])+`|"(?:""|[^"])+"|\\[[^\\]]+\\]|[A-Za-z_@$#][A-Za-z0-9_@$#]*';
|
|
102
|
+
const qualifiedIdentifierPattern = `(?:${identifierPattern})(?:\\s*\\.\\s*(?:${identifierPattern}))*`;
|
|
103
|
+
const qualifiedIdentifierOnlyRegex = new RegExp(`^\\s*${qualifiedIdentifierPattern}\\s*$`, 'i');
|
|
104
|
+
const aliasStopWordPattern =
|
|
105
|
+
'on|where|join|inner|left|right|full|outer|cross|straight_join|group|order|having|limit|union';
|
|
106
|
+
|
|
107
|
+
function splitTopLevelCommaList(text) {
|
|
108
|
+
const result = [];
|
|
109
|
+
let current = '';
|
|
110
|
+
let quote = null;
|
|
111
|
+
let depth = 0;
|
|
112
|
+
|
|
113
|
+
for (let index = 0; index < text.length; index++) {
|
|
114
|
+
const ch = text[index];
|
|
115
|
+
const next = text[index + 1];
|
|
116
|
+
|
|
117
|
+
if (quote) {
|
|
118
|
+
current += ch;
|
|
119
|
+
if (ch == quote) {
|
|
120
|
+
if ((quote == '"' || quote == '`') && next == quote) {
|
|
121
|
+
current += next;
|
|
122
|
+
index++;
|
|
123
|
+
} else {
|
|
124
|
+
quote = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (ch == '"' || ch == '`' || ch == '[') {
|
|
131
|
+
quote = ch == '[' ? ']' : ch;
|
|
132
|
+
current += ch;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch == '(') depth++;
|
|
136
|
+
if (ch == ')' && depth > 0) depth--;
|
|
137
|
+
if (ch == ',' && depth == 0) {
|
|
138
|
+
result.push(current.trim());
|
|
139
|
+
current = '';
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
current += ch;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (current.trim()) result.push(current.trim());
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractIdentifierParts(text) {
|
|
150
|
+
const identifierRegex = new RegExp(identifierPattern, 'g');
|
|
151
|
+
return (text.match(identifierRegex) || []).map(identifier => unquoteIdentifier(identifier));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function extractMySqlStyleViewColumnMetadata(selectSql, dbinfo) {
|
|
155
|
+
if (!selectSql) return null;
|
|
156
|
+
const selectMatch = selectSql.match(/^\s*select\s+([\s\S]+?)\s+from\s/i);
|
|
157
|
+
if (!selectMatch) return null;
|
|
158
|
+
|
|
159
|
+
const tableByAlias = {};
|
|
160
|
+
const tableRegex = new RegExp(
|
|
161
|
+
`\\b(?:from|join)\\s+\\(*\\s*(${qualifiedIdentifierPattern})(?:\\s+(?:as\\s+)?(?!\\b(?:${aliasStopWordPattern})\\b)(${identifierPattern}))?`,
|
|
162
|
+
'gi'
|
|
163
|
+
);
|
|
164
|
+
let tableMatch;
|
|
165
|
+
while ((tableMatch = tableRegex.exec(selectSql))) {
|
|
166
|
+
const tableParts = extractIdentifierParts(tableMatch[1]);
|
|
167
|
+
const alias = unquoteIdentifier(tableMatch[2]);
|
|
168
|
+
if (!tableParts.length) continue;
|
|
169
|
+
const metadata = {
|
|
170
|
+
schemaName: tableParts.length >= 2 ? tableParts[tableParts.length - 2] : undefined,
|
|
171
|
+
tableName: tableParts[tableParts.length - 1],
|
|
172
|
+
};
|
|
173
|
+
if (alias) tableByAlias[alias.toLowerCase()] = metadata;
|
|
174
|
+
tableByAlias[metadata.tableName.toLowerCase()] = metadata;
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(tableByAlias).length == 0) return null;
|
|
177
|
+
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const item of splitTopLevelCommaList(selectMatch[1])) {
|
|
180
|
+
const [sourceText, aliasText] = item.split(/\s+\bas\b\s+/i);
|
|
181
|
+
if (!qualifiedIdentifierOnlyRegex.test(sourceText)) continue;
|
|
182
|
+
const sourceParts = extractIdentifierParts(sourceText);
|
|
183
|
+
if (sourceParts.length < 2) continue;
|
|
184
|
+
const sourceColumnName = sourceParts[sourceParts.length - 1];
|
|
185
|
+
const qualifier = sourceParts[sourceParts.length - 2];
|
|
186
|
+
const table = tableByAlias[qualifier.toLowerCase()];
|
|
187
|
+
if (!table) continue;
|
|
188
|
+
const aliasParts = aliasText ? extractIdentifierParts(aliasText) : [];
|
|
189
|
+
const resultColumnName = aliasParts[0] || sourceColumnName;
|
|
190
|
+
result[resultColumnName] = resolveMetadataTable(
|
|
191
|
+
{
|
|
192
|
+
...table,
|
|
193
|
+
sourceColumnName,
|
|
194
|
+
},
|
|
195
|
+
dbinfo
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Object.keys(result).length == 0 ? null : result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveMetadataTable(metadata, dbinfo) {
|
|
203
|
+
if (!metadata?.tableName) return metadata;
|
|
204
|
+
const table = findTable(dbinfo, metadata.schemaName, metadata.tableName);
|
|
205
|
+
if (!table) return metadata;
|
|
206
|
+
return {
|
|
207
|
+
...metadata,
|
|
208
|
+
tableName: table.pureName,
|
|
209
|
+
schemaName: table.schemaName,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function createViewColumnMetadata(view, dbinfo) {
|
|
214
|
+
const selectSql = extractSelectFromCreateView(view?.createSql);
|
|
215
|
+
if (!selectSql) return null;
|
|
216
|
+
const viewColumnNames = getViewColumnNames(view);
|
|
217
|
+
const columnMetadata = extractColumnMetadataFromSql(selectSql) || extractMySqlStyleViewColumnMetadata(selectSql, dbinfo);
|
|
218
|
+
|
|
219
|
+
if (columnMetadata && Object.keys(columnMetadata).length > 0) {
|
|
220
|
+
if (!viewColumnNames?.length) {
|
|
221
|
+
return Object.fromEntries(
|
|
222
|
+
Object.entries(columnMetadata).map(([columnName, metadata]) => [
|
|
223
|
+
columnName,
|
|
224
|
+
resolveMetadataTable(metadata, dbinfo),
|
|
225
|
+
])
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const metadataValues = Object.values(columnMetadata).map(metadata => resolveMetadataTable(metadata, dbinfo));
|
|
230
|
+
const allowPositionalFallback =
|
|
231
|
+
metadataValues.length == viewColumnNames.length && metadataValues.length == getSelectListItemCount(selectSql);
|
|
232
|
+
const result = {};
|
|
233
|
+
for (let index = 0; index < viewColumnNames.length; index++) {
|
|
234
|
+
const columnName = viewColumnNames[index];
|
|
235
|
+
const metadata = getColumnMetadata(columnMetadata, columnName);
|
|
236
|
+
const resolvedMetadata = metadata
|
|
237
|
+
? resolveMetadataTable(metadata, dbinfo)
|
|
238
|
+
: allowPositionalFallback
|
|
239
|
+
? metadataValues[index]
|
|
240
|
+
: null;
|
|
241
|
+
if (resolvedMetadata) result[columnName] = resolvedMetadata;
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!viewColumnNames?.length || !isSelectStarFromSingleTable(selectSql)) return columnMetadata;
|
|
247
|
+
const sourceTable = extractSingleTableFromSql(selectSql);
|
|
248
|
+
const table = findTable(dbinfo, sourceTable?.schemaName, sourceTable?.tableName);
|
|
249
|
+
if (!table?.columns?.length) return columnMetadata;
|
|
250
|
+
|
|
251
|
+
const result = {};
|
|
252
|
+
for (let index = 0; index < viewColumnNames.length; index++) {
|
|
253
|
+
const sourceColumn = table.columns[index];
|
|
254
|
+
if (!sourceColumn?.columnName) continue;
|
|
255
|
+
result[viewColumnNames[index]] = {
|
|
256
|
+
tableName: table.pureName,
|
|
257
|
+
schemaName: table.schemaName,
|
|
258
|
+
sourceColumnName: sourceColumn.columnName,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveViewResultColumns(columns, dbinfo) {
|
|
265
|
+
if (!columns?.length) return columns;
|
|
266
|
+
if (!dbinfo?.views?.length && !dbinfo?.matviews?.length) return columns;
|
|
267
|
+
|
|
268
|
+
return columns.map(column => {
|
|
269
|
+
const view = findView(dbinfo, column.tableSchema, column.tableName);
|
|
270
|
+
if (!view?.createSql) return column;
|
|
271
|
+
|
|
272
|
+
const columnMetadata = createViewColumnMetadata(view, dbinfo);
|
|
273
|
+
const metadata = getColumnMetadata(columnMetadata, column.columnName);
|
|
274
|
+
if (!metadata) return column;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
...column,
|
|
278
|
+
tableName: metadata.tableName,
|
|
279
|
+
tableSchema: metadata.schemaName,
|
|
280
|
+
sourceColumnName: metadata.sourceColumnName,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function enrichQueryResultColumns({ columns, sql, driver, dbhan, dbinfo, onNativeMetadataError, onFallbackMetadataError }) {
|
|
286
|
+
if (!columns?.length || !driver?.databaseEngineTypes?.includes('sql') || !driver?.supportsEditableQueryResults) {
|
|
287
|
+
return columns;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (driver.enrichColumnMetadata) {
|
|
291
|
+
try {
|
|
292
|
+
const enriched = resolveViewResultColumns(await driver.enrichColumnMetadata(dbhan, sql, columns, dbinfo), dbinfo);
|
|
293
|
+
if (enriched?.every(column => column.tableName && column.sourceColumnName)) return enriched;
|
|
294
|
+
if (enriched?.length == columns.length) columns = enriched;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
onNativeMetadataError?.(err);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const columnMetadata = extractColumnMetadataFromSql(sql);
|
|
302
|
+
if (columnMetadata) {
|
|
303
|
+
return resolveViewResultColumns(columns.map(column => {
|
|
304
|
+
const metadata = getColumnMetadata(columnMetadata, column.columnName);
|
|
305
|
+
return {
|
|
306
|
+
...column,
|
|
307
|
+
tableName: column.tableName || metadata?.tableName,
|
|
308
|
+
tableSchema: column.tableSchema || metadata?.schemaName,
|
|
309
|
+
sourceColumnName: column.sourceColumnName || metadata?.sourceColumnName,
|
|
310
|
+
};
|
|
311
|
+
}), dbinfo);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const table = extractSingleTableFromSql(sql);
|
|
315
|
+
if (!table) return columns;
|
|
316
|
+
const columnSources = extractColumnSourcesFromSql(sql);
|
|
317
|
+
|
|
318
|
+
return resolveViewResultColumns(columns.map(column => ({
|
|
319
|
+
...column,
|
|
320
|
+
tableName: column.tableName || table.tableName,
|
|
321
|
+
tableSchema: column.tableSchema || table.schemaName,
|
|
322
|
+
sourceColumnName: column.sourceColumnName || getColumnSourceName(columnSources, column.columnName) || column.columnName,
|
|
323
|
+
})), dbinfo);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
onFallbackMetadataError?.(err);
|
|
326
|
+
return columns;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
enrichQueryResultColumns,
|
|
332
|
+
resolveViewResultColumns,
|
|
333
|
+
};
|