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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api-premium",
3
3
  "main": "src/index.js",
4
- "version": "7.1.13",
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.1.13",
33
+ "dbgate-datalib": "7.2.0",
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.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.1.13",
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);
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '7.1.13',
4
- buildTime: '2026-05-27T11:36:05.885Z'
3
+ version: '7.2.0',
4
+ buildTime: '2026-06-08T11:54:20.800Z'
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
  }
@@ -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 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
 
@@ -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.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
  }
@@ -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
+ };