dbgate-tools 7.1.12 → 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.
@@ -24,6 +24,7 @@ export declare const driverBase: {
24
24
  databaseEngineTypes: string[];
25
25
  supportedCreateDatabase: boolean;
26
26
  supportExecuteQuery: boolean;
27
+ supportsEditableQueryResults: boolean;
27
28
  analyseFull(pool: any, version: any): Promise<any>;
28
29
  analyseSingleObject(pool: any, name: any, typeField?: string): Promise<any>;
29
30
  analyseSingleTable(pool: any, name: any): any;
package/lib/driverBase.js CHANGED
@@ -72,6 +72,7 @@ exports.driverBase = {
72
72
  databaseEngineTypes: ['sql'],
73
73
  supportedCreateDatabase: true,
74
74
  supportExecuteQuery: true,
75
+ supportsEditableQueryResults: true,
75
76
  async analyseFull(pool, version) {
76
77
  const analyser = new this.analyserClass(pool, this, version);
77
78
  return analyser.fullAnalysis();
package/lib/index.d.ts CHANGED
@@ -27,3 +27,4 @@ export * from './schemaInfoTools';
27
27
  export * from './redisKeysLoader';
28
28
  export * from './rowProgressReporter';
29
29
  export * from './diagramTools';
30
+ export * from './queryMetadataTools';
package/lib/index.js CHANGED
@@ -43,3 +43,4 @@ __exportStar(require("./schemaInfoTools"), exports);
43
43
  __exportStar(require("./redisKeysLoader"), exports);
44
44
  __exportStar(require("./rowProgressReporter"), exports);
45
45
  __exportStar(require("./diagramTools"), exports);
46
+ __exportStar(require("./queryMetadataTools"), exports);
@@ -0,0 +1,17 @@
1
+ type ColumnSourceMetadata = {
2
+ tableName: string;
3
+ schemaName?: string;
4
+ sourceColumnName: string;
5
+ };
6
+ export declare function extractSingleTableFromSql(sql: string): {
7
+ tableName: string;
8
+ schemaName?: string;
9
+ } | null;
10
+ export declare function isSimpleSelectQuery(sql: string): boolean;
11
+ export declare function extractColumnSourcesFromSql(sql: string): {
12
+ [resultColumnName: string]: string;
13
+ } | null;
14
+ export declare function extractColumnMetadataFromSql(sql: string): {
15
+ [resultColumnName: string]: ColumnSourceMetadata;
16
+ } | null;
17
+ export {};
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractColumnMetadataFromSql = exports.extractColumnSourcesFromSql = exports.isSimpleSelectQuery = exports.extractSingleTableFromSql = void 0;
4
+ const CLAUSE_KEYWORDS = new Set(['where', 'order', 'limit', 'offset', 'fetch', 'for']);
5
+ const BLOCKED_KEYWORDS = [
6
+ 'join',
7
+ 'group',
8
+ 'having',
9
+ 'distinct',
10
+ 'union',
11
+ 'with',
12
+ 'insert',
13
+ 'update',
14
+ 'delete',
15
+ 'merge',
16
+ ];
17
+ const AGGREGATE_FUNCTIONS = ['count', 'sum', 'avg', 'min', 'max'];
18
+ const JOIN_TYPE_KEYWORDS = new Set(['inner', 'left', 'right', 'full', 'cross', 'outer']);
19
+ const TABLE_ALIAS_STOP_KEYWORDS = new Set([
20
+ ...CLAUSE_KEYWORDS,
21
+ 'join',
22
+ 'inner',
23
+ 'left',
24
+ 'right',
25
+ 'full',
26
+ 'cross',
27
+ 'outer',
28
+ 'on',
29
+ ]);
30
+ function stripCommentsAndStrings(sql) {
31
+ let result = '';
32
+ let i = 0;
33
+ while (i < sql.length) {
34
+ const ch = sql[i];
35
+ const next = sql[i + 1];
36
+ if (ch == '-' && next == '-') {
37
+ while (i < sql.length && sql[i] != '\n')
38
+ i++;
39
+ result += ' ';
40
+ continue;
41
+ }
42
+ if (ch == '/' && next == '*') {
43
+ i += 2;
44
+ while (i < sql.length && !(sql[i] == '*' && sql[i + 1] == '/'))
45
+ i++;
46
+ i += 2;
47
+ result += ' ';
48
+ continue;
49
+ }
50
+ if (ch == "'") {
51
+ result += ' ';
52
+ i++;
53
+ while (i < sql.length) {
54
+ if (sql[i] == "'" && sql[i + 1] == "'") {
55
+ i += 2;
56
+ continue;
57
+ }
58
+ if (sql[i] == "'") {
59
+ i++;
60
+ break;
61
+ }
62
+ i++;
63
+ }
64
+ continue;
65
+ }
66
+ result += ch;
67
+ i++;
68
+ }
69
+ return result;
70
+ }
71
+ function tokenize(sql) {
72
+ const tokens = [];
73
+ let i = 0;
74
+ while (i < sql.length) {
75
+ const ch = sql[i];
76
+ if (/\s/.test(ch)) {
77
+ i++;
78
+ continue;
79
+ }
80
+ if (ch == '"' || ch == '`') {
81
+ const quote = ch;
82
+ let text = '';
83
+ i++;
84
+ while (i < sql.length) {
85
+ if (sql[i] == quote && sql[i + 1] == quote) {
86
+ text += quote;
87
+ i += 2;
88
+ continue;
89
+ }
90
+ if (sql[i] == quote) {
91
+ i++;
92
+ break;
93
+ }
94
+ text += sql[i];
95
+ i++;
96
+ }
97
+ tokens.push({ text, lower: text.toLowerCase() });
98
+ continue;
99
+ }
100
+ if (ch == '[') {
101
+ let text = '';
102
+ i++;
103
+ while (i < sql.length) {
104
+ if (sql[i] == ']' && sql[i + 1] == ']') {
105
+ text += ']';
106
+ i += 2;
107
+ continue;
108
+ }
109
+ if (sql[i] == ']') {
110
+ i++;
111
+ break;
112
+ }
113
+ text += sql[i];
114
+ i++;
115
+ }
116
+ tokens.push({ text, lower: text.toLowerCase() });
117
+ continue;
118
+ }
119
+ if (/[A-Za-z_@$#]/.test(ch)) {
120
+ let text = ch;
121
+ i++;
122
+ while (i < sql.length && /[A-Za-z0-9_@$#]/.test(sql[i])) {
123
+ text += sql[i];
124
+ i++;
125
+ }
126
+ tokens.push({ text, lower: text.toLowerCase() });
127
+ continue;
128
+ }
129
+ if (/[0-9]/.test(ch)) {
130
+ let text = ch;
131
+ i++;
132
+ while (i < sql.length && /[0-9.]/.test(sql[i])) {
133
+ text += sql[i];
134
+ i++;
135
+ }
136
+ tokens.push({ text, lower: text.toLowerCase() });
137
+ continue;
138
+ }
139
+ tokens.push({ text: ch, lower: ch });
140
+ i++;
141
+ }
142
+ return tokens;
143
+ }
144
+ function splitTopLevelStatements(tokens) {
145
+ const statements = [];
146
+ let current = [];
147
+ for (const token of tokens) {
148
+ if (token.text == ';') {
149
+ if (current.length > 0)
150
+ statements.push(current);
151
+ current = [];
152
+ }
153
+ else {
154
+ current.push(token);
155
+ }
156
+ }
157
+ if (current.length > 0)
158
+ statements.push(current);
159
+ return statements;
160
+ }
161
+ function tokenMatches(tokens, index, text) {
162
+ var _a;
163
+ return ((_a = tokens[index]) === null || _a === void 0 ? void 0 : _a.lower) == text;
164
+ }
165
+ function parseQualifiedIdentifier(tokens, start) {
166
+ var _a;
167
+ const parts = [];
168
+ let index = start;
169
+ if (!tokens[index] || !/^[A-Za-z_@$#][A-Za-z0-9_@$#]*$/.test(tokens[index].text))
170
+ return null;
171
+ parts.push(tokens[index].text);
172
+ index++;
173
+ while (((_a = tokens[index]) === null || _a === void 0 ? void 0 : _a.text) == '.') {
174
+ index++;
175
+ if (!tokens[index] || !/^[A-Za-z_@$#][A-Za-z0-9_@$#]*$/.test(tokens[index].text))
176
+ return null;
177
+ parts.push(tokens[index].text);
178
+ index++;
179
+ }
180
+ return { parts, index };
181
+ }
182
+ function getSingleStatementTokens(sql) {
183
+ const cleanSql = stripCommentsAndStrings(sql).trim();
184
+ const tokens = tokenize(cleanSql);
185
+ const statements = splitTopLevelStatements(tokens);
186
+ return statements.length == 1 ? statements[0] : null;
187
+ }
188
+ function hasBlockedConstruct(tokens, allowJoin = false) {
189
+ var _a, _b;
190
+ if (((_a = tokens[0]) === null || _a === void 0 ? void 0 : _a.lower) == 'with')
191
+ return true;
192
+ for (let i = 0; i < tokens.length; i++) {
193
+ const token = tokens[i];
194
+ if (allowJoin && token.lower == 'join')
195
+ continue;
196
+ if (BLOCKED_KEYWORDS.includes(token.lower))
197
+ return true;
198
+ if (token.lower == 'select' && i > 0)
199
+ return true;
200
+ if (token.text == '(' || token.text == ')')
201
+ return true;
202
+ if (AGGREGATE_FUNCTIONS.includes(token.lower) && ((_b = tokens[i + 1]) === null || _b === void 0 ? void 0 : _b.text) == '(')
203
+ return true;
204
+ }
205
+ return false;
206
+ }
207
+ function validateSelectList(tokens, selectIndex, fromIndex) {
208
+ return extractSimpleSelectColumnSources(tokens, selectIndex, fromIndex) != null;
209
+ }
210
+ function isIdentifierToken(token) {
211
+ return !!token && /^[A-Za-z_@$#][A-Za-z0-9_@$#]*$/.test(token.text);
212
+ }
213
+ function splitSelectListItems(tokens, selectIndex, fromIndex) {
214
+ if (fromIndex <= selectIndex + 1)
215
+ return null;
216
+ let current = [];
217
+ const items = [];
218
+ for (let i = selectIndex + 1; i < fromIndex; i++) {
219
+ if (tokens[i].text == ',') {
220
+ if (current.length == 0)
221
+ return false;
222
+ items.push(current);
223
+ current = [];
224
+ }
225
+ else {
226
+ current.push(tokens[i]);
227
+ }
228
+ }
229
+ if (current.length == 0)
230
+ return false;
231
+ items.push(current);
232
+ return items;
233
+ }
234
+ function parseSelectListItem(item) {
235
+ var _a;
236
+ if (item.length == 1 && item[0].text == '*')
237
+ return { wildcard: true };
238
+ const parsed = parseQualifiedIdentifier(item, 0);
239
+ if (!parsed || parsed.parts.length < 1 || parsed.parts.length > 2)
240
+ return null;
241
+ const sourceColumnName = parsed.parts[parsed.parts.length - 1];
242
+ let resultColumnName = sourceColumnName;
243
+ let index = parsed.index;
244
+ if (((_a = item[index]) === null || _a === void 0 ? void 0 : _a.lower) == 'as') {
245
+ index++;
246
+ if (!isIdentifierToken(item[index]))
247
+ return null;
248
+ resultColumnName = item[index].text;
249
+ index++;
250
+ }
251
+ else if (item[index]) {
252
+ if (!isIdentifierToken(item[index]))
253
+ return null;
254
+ resultColumnName = item[index].text;
255
+ index++;
256
+ }
257
+ if (index != item.length)
258
+ return null;
259
+ return { sourceColumnName, resultColumnName };
260
+ }
261
+ function parseQualifiedSelectListItem(item) {
262
+ var _a;
263
+ const parsed = parseQualifiedIdentifier(item, 0);
264
+ if (!parsed || parsed.parts.length < 2 || parsed.parts.length > 3)
265
+ return null;
266
+ const sourceColumnName = parsed.parts[parsed.parts.length - 1];
267
+ const qualifier = parsed.parts.slice(0, -1).join('.');
268
+ let resultColumnName = sourceColumnName;
269
+ let index = parsed.index;
270
+ if (((_a = item[index]) === null || _a === void 0 ? void 0 : _a.lower) == 'as') {
271
+ index++;
272
+ if (!isIdentifierToken(item[index]))
273
+ return null;
274
+ resultColumnName = item[index].text;
275
+ index++;
276
+ }
277
+ else if (item[index]) {
278
+ if (!isIdentifierToken(item[index]))
279
+ return null;
280
+ resultColumnName = item[index].text;
281
+ index++;
282
+ }
283
+ if (index != item.length)
284
+ return null;
285
+ return { qualifier, sourceColumnName, resultColumnName };
286
+ }
287
+ function extractSimpleSelectColumnSources(tokens, selectIndex, fromIndex) {
288
+ const items = splitSelectListItems(tokens, selectIndex, fromIndex);
289
+ if (!items)
290
+ return null;
291
+ const result = [];
292
+ for (const item of items) {
293
+ const parsed = parseSelectListItem(item);
294
+ if (!parsed)
295
+ return null;
296
+ if (!parsed.wildcard) {
297
+ result.push({
298
+ resultColumnName: parsed.resultColumnName,
299
+ sourceColumnName: parsed.sourceColumnName,
300
+ });
301
+ }
302
+ }
303
+ return result;
304
+ }
305
+ function getTableNameFromParts(parts) {
306
+ return {
307
+ schemaName: parts.length >= 2 ? parts[parts.length - 2] : undefined,
308
+ tableName: parts[parts.length - 1],
309
+ };
310
+ }
311
+ function addResolvedTable(resolvedTables, key, table) {
312
+ const lowerKey = key.toLowerCase();
313
+ const existingTables = resolvedTables.get(lowerKey) || [];
314
+ if (existingTables.some(item => item.schemaName == table.schemaName && item.tableName == table.tableName))
315
+ return;
316
+ resolvedTables.set(lowerKey, [...existingTables, table]);
317
+ }
318
+ function parseFromTables(tokens, fromIndex) {
319
+ var _a;
320
+ const resolvedTables = new Map();
321
+ let index = fromIndex + 1;
322
+ let expectTable = true;
323
+ while (index < tokens.length) {
324
+ const token = tokens[index];
325
+ if (CLAUSE_KEYWORDS.has(token.lower))
326
+ break;
327
+ if (token.lower == 'on') {
328
+ expectTable = false;
329
+ index++;
330
+ continue;
331
+ }
332
+ if (token.lower == 'join') {
333
+ expectTable = true;
334
+ index++;
335
+ continue;
336
+ }
337
+ if (JOIN_TYPE_KEYWORDS.has(token.lower)) {
338
+ index++;
339
+ continue;
340
+ }
341
+ if (!expectTable) {
342
+ index++;
343
+ continue;
344
+ }
345
+ const parsed = parseQualifiedIdentifier(tokens, index);
346
+ if (!parsed || parsed.parts.length < 1 || parsed.parts.length > 3)
347
+ return null;
348
+ const { schemaName, tableName } = getTableNameFromParts(parsed.parts);
349
+ const table = { schemaName, tableName, sourceColumnName: null };
350
+ addResolvedTable(resolvedTables, tableName, table);
351
+ if (schemaName)
352
+ addResolvedTable(resolvedTables, `${schemaName}.${tableName}`, table);
353
+ addResolvedTable(resolvedTables, parsed.parts.join('.'), table);
354
+ index = parsed.index;
355
+ if (((_a = tokens[index]) === null || _a === void 0 ? void 0 : _a.lower) == 'as')
356
+ index++;
357
+ if (tokens[index] && !TABLE_ALIAS_STOP_KEYWORDS.has(tokens[index].lower) && tokens[index].text != ',') {
358
+ if (!isIdentifierToken(tokens[index]))
359
+ return null;
360
+ addResolvedTable(resolvedTables, tokens[index].text, table);
361
+ index++;
362
+ }
363
+ expectTable = false;
364
+ }
365
+ return resolvedTables;
366
+ }
367
+ function resolveTable(resolvedTables, qualifier) {
368
+ const matchingTables = resolvedTables.get(qualifier.toLowerCase()) || [];
369
+ if (matchingTables.length != 1)
370
+ return null;
371
+ return matchingTables[0];
372
+ }
373
+ function extractJoinColumnMetadata(tokens, selectIndex, fromIndex) {
374
+ const items = splitSelectListItems(tokens, selectIndex, fromIndex);
375
+ if (!items)
376
+ return null;
377
+ const resolvedTables = parseFromTables(tokens, fromIndex);
378
+ if (!resolvedTables)
379
+ return null;
380
+ const result = [];
381
+ for (const item of items) {
382
+ const parsed = parseQualifiedSelectListItem(item);
383
+ if (!parsed)
384
+ continue;
385
+ const table = resolveTable(resolvedTables, parsed.qualifier);
386
+ if (!table)
387
+ continue;
388
+ result.push({
389
+ resultColumnName: parsed.resultColumnName,
390
+ metadata: {
391
+ schemaName: table.schemaName,
392
+ tableName: table.tableName,
393
+ sourceColumnName: parsed.sourceColumnName,
394
+ },
395
+ });
396
+ }
397
+ return result;
398
+ }
399
+ function extractSingleTableFromSql(sql) {
400
+ const tokens = getSingleStatementTokens(sql);
401
+ if (!tokens || hasBlockedConstruct(tokens))
402
+ return null;
403
+ if (!tokenMatches(tokens, 0, 'select'))
404
+ return null;
405
+ const fromIndex = tokens.findIndex(token => token.lower == 'from');
406
+ if (fromIndex <= 0)
407
+ return null;
408
+ if (!validateSelectList(tokens, 0, fromIndex))
409
+ return null;
410
+ const parsedTable = parseQualifiedIdentifier(tokens, fromIndex + 1);
411
+ if (!parsedTable || parsedTable.parts.length < 1 || parsedTable.parts.length > 2)
412
+ return null;
413
+ let index = parsedTable.index;
414
+ if (tokens[index] && !CLAUSE_KEYWORDS.has(tokens[index].lower)) {
415
+ if (tokens[index].lower == 'as')
416
+ index++;
417
+ if (!tokens[index] || CLAUSE_KEYWORDS.has(tokens[index].lower))
418
+ return null;
419
+ if (tokens[index].text == ',' || tokens[index].text == '.')
420
+ return null;
421
+ index++;
422
+ }
423
+ if (index < tokens.length && !CLAUSE_KEYWORDS.has(tokens[index].lower))
424
+ return null;
425
+ const [schemaName, tableName] = parsedTable.parts.length == 2 ? [parsedTable.parts[0], parsedTable.parts[1]] : [undefined, parsedTable.parts[0]];
426
+ return { tableName, schemaName };
427
+ }
428
+ exports.extractSingleTableFromSql = extractSingleTableFromSql;
429
+ function isSimpleSelectQuery(sql) {
430
+ return extractSingleTableFromSql(sql) != null;
431
+ }
432
+ exports.isSimpleSelectQuery = isSimpleSelectQuery;
433
+ function extractColumnSourcesFromSql(sql) {
434
+ const tokens = getSingleStatementTokens(sql);
435
+ if (!tokens || hasBlockedConstruct(tokens))
436
+ return null;
437
+ if (!tokenMatches(tokens, 0, 'select'))
438
+ return null;
439
+ const fromIndex = tokens.findIndex(token => token.lower == 'from');
440
+ if (fromIndex <= 0)
441
+ return null;
442
+ const columns = extractSimpleSelectColumnSources(tokens, 0, fromIndex);
443
+ if (!columns)
444
+ return null;
445
+ return Object.fromEntries(columns.map(column => [column.resultColumnName, column.sourceColumnName]));
446
+ }
447
+ exports.extractColumnSourcesFromSql = extractColumnSourcesFromSql;
448
+ function extractColumnMetadataFromSql(sql) {
449
+ const tokens = getSingleStatementTokens(sql);
450
+ if (!tokens || hasBlockedConstruct(tokens, true))
451
+ return null;
452
+ if (!tokenMatches(tokens, 0, 'select'))
453
+ return null;
454
+ const fromIndex = tokens.findIndex(token => token.lower == 'from');
455
+ if (fromIndex <= 0)
456
+ return null;
457
+ const table = extractSingleTableFromSql(sql);
458
+ if (table) {
459
+ const columns = extractSimpleSelectColumnSources(tokens, 0, fromIndex);
460
+ if (!columns)
461
+ return null;
462
+ return Object.fromEntries(columns.map(column => [
463
+ column.resultColumnName,
464
+ {
465
+ tableName: table.tableName,
466
+ schemaName: table.schemaName,
467
+ sourceColumnName: column.sourceColumnName,
468
+ },
469
+ ]));
470
+ }
471
+ const columns = extractJoinColumnMetadata(tokens, 0, fromIndex);
472
+ if (!columns)
473
+ return null;
474
+ return Object.fromEntries(columns.map(column => [column.resultColumnName, column.metadata]));
475
+ }
476
+ exports.extractColumnMetadataFromSql = extractColumnMetadataFromSql;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "7.1.12",
2
+ "version": "7.2.0",
3
3
  "name": "dbgate-tools",
4
4
  "main": "lib/index.js",
5
5
  "typings": "lib/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "devDependencies": {
28
28
  "@types/node": "^13.7.0",
29
- "dbgate-types": "7.1.12",
29
+ "dbgate-types": "7.2.0",
30
30
  "jest": "^28.1.3",
31
31
  "ts-jest": "^28.0.7",
32
32
  "typescript": "^4.4.3"
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "blueimp-md5": "^2.19.0",
36
36
  "dbgate-query-splitter": "^4.12.0",
37
- "dbgate-sqltree": "7.1.12",
37
+ "dbgate-sqltree": "7.2.0",
38
38
  "debug": "^4.3.4",
39
39
  "json-stable-stringify": "^1.0.1",
40
40
  "lodash": "^4.17.21",