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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api",
3
3
  "main": "src/index.js",
4
- "version": "7.1.13",
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.13",
33
+ "dbgate-datalib": "7.2.1",
34
34
  "dbgate-query-splitter": "^4.12.0",
35
- "dbgate-rest": "7.1.13",
36
- "dbgate-sqltree": "7.1.13",
37
- "dbgate-tools": "7.1.13",
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.13",
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 { runid } = progressName;
161
- logger.error(`DBGM-00103 Error in database connection ${conid}, database ${database}: ${copyStreamError}`);
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);
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '7.1.13',
4
- buildTime: '2026-05-27T11:35:48.071Z'
3
+ version: '7.2.1',
4
+ buildTime: '2026-06-19T10:21:38.466Z'
5
5
  };
@@ -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
- const evalWriter = new ScriptWriterEval(dbgateApi, requirePlugin, dbhan, runid);
577
- await playJsonScriptWriter(script, evalWriter);
578
- process.send({ msgtype: 'runnerDone', runid });
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 without read-only sessions is read only',
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 without read-only sessions is read only',
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('end', () => {
231
- writer.close(() => {
232
- process.send({ msgtype: 'done' });
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,
@@ -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
- // throw err;
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.rowCounter = 0;
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
- recordset(columns, options) {
195
+ async prepareRecordset(recordsetContext, columns, options) {
184
196
  if (this.rowsLimitOverflow) {
185
197
  return;
186
198
  }
187
199
  this.closeCurrentWriter();
188
- this.currentWriter = new QueryStreamTableWriter(this.sesid);
189
- this.currentWriter.initializeFromQuery(
190
- Array.isArray(columns) ? { columns } : columns,
191
- this.queryStreamInfoHolder.resultIndex,
192
- this.frontMatter?.[`chart-${this.queryStreamInfoHolder.resultIndex + 1}`],
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
- this.queryStreamInfoHolder.resultIndex += 1;
197
- this.rowCounter = 0;
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
- if (this.limitRows && this.rowCounter >= this.limitRows) {
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 ${this.rowCounter} rows, canceling query`, severity: 'error' },
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 (this.currentWriter) {
233
- this.currentWriter.row(row);
234
- this.rowCounter += 1;
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
- this.closeCurrentWriter();
245
- // currentHandlers = currentHandlers.filter((x) => x != this);
246
- this.resolve();
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
- sessionCount: sessions.opened.length,
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
+ };