@hesed/mysql 0.5.1 → 0.6.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/README.md +53 -50
- package/dist/base-command.d.ts +0 -2
- package/dist/base-command.js +0 -13
- package/dist/commands/mysql/auth/add.js +14 -0
- package/dist/commands/mysql/auth/update.js +14 -0
- package/dist/commands/mysql/databases.d.ts +4 -5
- package/dist/commands/mysql/databases.js +7 -8
- package/dist/commands/mysql/describe-table.d.ts +3 -2
- package/dist/commands/mysql/describe-table.js +6 -9
- package/dist/commands/mysql/{explain-query.d.ts → explain.d.ts} +3 -2
- package/dist/commands/mysql/{explain-query.js → explain.js} +6 -9
- package/dist/commands/mysql/indexes.d.ts +3 -2
- package/dist/commands/mysql/indexes.js +6 -9
- package/dist/commands/mysql/query.d.ts +3 -2
- package/dist/commands/mysql/query.js +13 -16
- package/dist/commands/mysql/tables.d.ts +4 -4
- package/dist/commands/mysql/tables.js +7 -7
- package/dist/mysql/config-loader.d.ts +4 -0
- package/dist/mysql/database.d.ts +39 -30
- package/dist/mysql/mysql-client.js +2 -0
- package/dist/mysql/mysql-utils.d.ts +6 -2
- package/dist/mysql/mysql-utils.js +149 -57
- package/oclif.manifest.json +77 -63
- package/package.json +2 -2
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
3
|
import { closeConnections, listTables } from '../../mysql/index.js';
|
|
3
|
-
export default class MySQLTables extends
|
|
4
|
+
export default class MySQLTables extends BaseCommand {
|
|
4
5
|
static description = 'List all tables in the current MySQL database';
|
|
5
6
|
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> -p local'];
|
|
6
7
|
static flags = {
|
|
7
8
|
profile: Flags.string({ char: 'p', description: 'Database profile name from config', required: false }),
|
|
8
9
|
};
|
|
9
|
-
jsonEnabled() {
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
10
|
async run() {
|
|
13
11
|
const { flags } = await this.parse(MySQLTables);
|
|
14
12
|
const result = await listTables(this.config, flags.profile);
|
|
15
13
|
await closeConnections();
|
|
16
14
|
if (result.success) {
|
|
17
|
-
|
|
15
|
+
this.log(result.data?.result ?? '');
|
|
16
|
+
delete result.data.result;
|
|
17
|
+
return result;
|
|
18
18
|
}
|
|
19
|
-
this.error(result.error ?? 'Failed to list tables');
|
|
19
|
+
this.error(String(result.error ?? 'Failed to list tables'));
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -2,14 +2,18 @@ import type { ConnectionOptions as MySQL2ConnectionOptions } from 'mysql2/promis
|
|
|
2
2
|
export interface DatabaseProfile {
|
|
3
3
|
database: string;
|
|
4
4
|
host: string;
|
|
5
|
+
maxConcurrentQueries?: number;
|
|
5
6
|
password: string;
|
|
6
7
|
port: number;
|
|
8
|
+
queryQueueTimeoutMs?: number;
|
|
7
9
|
ssl?: boolean;
|
|
8
10
|
user: string;
|
|
9
11
|
}
|
|
10
12
|
interface SafetyConfig {
|
|
11
13
|
blacklistedOperations: string[];
|
|
12
14
|
defaultLimit: number;
|
|
15
|
+
maxConcurrentQueries?: number;
|
|
16
|
+
queryQueueTimeoutMs?: number;
|
|
13
17
|
requireConfirmationFor: string[];
|
|
14
18
|
}
|
|
15
19
|
export interface MySQLConfig {
|
package/dist/mysql/database.d.ts
CHANGED
|
@@ -1,49 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { ApiResult } from '@hesed/plugin-lib';
|
|
2
|
+
export type OutputFormat = 'csv' | 'json' | 'table' | 'toon';
|
|
3
|
+
export interface QueryData {
|
|
3
4
|
message?: string;
|
|
4
5
|
notices?: string;
|
|
5
6
|
requiresConfirmation?: boolean;
|
|
6
|
-
result?:
|
|
7
|
-
success: boolean;
|
|
7
|
+
result?: unknown;
|
|
8
8
|
}
|
|
9
|
-
export interface
|
|
10
|
-
databases
|
|
11
|
-
error?: string;
|
|
9
|
+
export interface DatabaseListData {
|
|
10
|
+
databases: string[];
|
|
12
11
|
result?: string;
|
|
13
|
-
success: boolean;
|
|
14
12
|
}
|
|
15
|
-
export interface
|
|
16
|
-
error?: string;
|
|
13
|
+
export interface TableListData {
|
|
17
14
|
result?: string;
|
|
18
|
-
|
|
19
|
-
tables?: string[];
|
|
15
|
+
tables: string[];
|
|
20
16
|
}
|
|
21
|
-
export interface
|
|
22
|
-
error?: string;
|
|
17
|
+
export interface TableStructureData {
|
|
23
18
|
result?: string;
|
|
24
|
-
structure
|
|
25
|
-
success: boolean;
|
|
19
|
+
structure: Record<string, unknown>[];
|
|
26
20
|
}
|
|
27
|
-
export interface
|
|
28
|
-
|
|
29
|
-
indexes?: Record<string, unknown>[];
|
|
21
|
+
export interface IndexData {
|
|
22
|
+
indexes: Record<string, unknown>[];
|
|
30
23
|
result?: string;
|
|
31
|
-
success: boolean;
|
|
32
24
|
}
|
|
33
|
-
export interface
|
|
34
|
-
|
|
35
|
-
plan?: Record<string, unknown>[];
|
|
25
|
+
export interface ExplainData {
|
|
26
|
+
plan: Record<string, unknown>[];
|
|
36
27
|
result?: string;
|
|
37
|
-
success: boolean;
|
|
38
28
|
}
|
|
39
|
-
|
|
40
|
-
database
|
|
41
|
-
error?: string;
|
|
29
|
+
interface ConnectionTestData {
|
|
30
|
+
database: string;
|
|
42
31
|
result?: string;
|
|
43
|
-
|
|
44
|
-
version?: string;
|
|
32
|
+
version: string;
|
|
45
33
|
}
|
|
46
|
-
export type
|
|
34
|
+
export type QueryResult = ApiResult & {
|
|
35
|
+
data?: QueryData;
|
|
36
|
+
};
|
|
37
|
+
export type DatabaseListResult = ApiResult & {
|
|
38
|
+
data?: DatabaseListData;
|
|
39
|
+
};
|
|
40
|
+
export type TableListResult = ApiResult & {
|
|
41
|
+
data?: TableListData;
|
|
42
|
+
};
|
|
43
|
+
export type TableStructureResult = ApiResult & {
|
|
44
|
+
data?: TableStructureData;
|
|
45
|
+
};
|
|
46
|
+
export type IndexResult = ApiResult & {
|
|
47
|
+
data?: IndexData;
|
|
48
|
+
};
|
|
49
|
+
export type ExplainResult = ApiResult & {
|
|
50
|
+
data?: ExplainData;
|
|
51
|
+
};
|
|
52
|
+
export type ConnectionTestResult = ApiResult & {
|
|
53
|
+
data?: ConnectionTestData;
|
|
54
|
+
};
|
|
47
55
|
export interface DatabaseUtil {
|
|
48
56
|
closeAll(): Promise<void>;
|
|
49
57
|
describeTable(profileName: string, table: string, format?: OutputFormat): Promise<TableStructureResult>;
|
|
@@ -53,3 +61,4 @@ export interface DatabaseUtil {
|
|
|
53
61
|
listTables(profileName: string): Promise<TableListResult>;
|
|
54
62
|
showIndexes(profileName: string, table: string, format?: OutputFormat): Promise<IndexResult>;
|
|
55
63
|
}
|
|
64
|
+
export {};
|
|
@@ -5,6 +5,8 @@ let cachedConfig = null;
|
|
|
5
5
|
const DEFAULT_SAFETY_CONFIG = {
|
|
6
6
|
blacklistedOperations: ['DROP DATABASE'],
|
|
7
7
|
defaultLimit: 100,
|
|
8
|
+
maxConcurrentQueries: 5,
|
|
9
|
+
queryQueueTimeoutMs: 60_000,
|
|
8
10
|
requireConfirmationFor: ['DELETE', 'UPDATE', 'DROP', 'TRUNCATE', 'ALTER'],
|
|
9
11
|
};
|
|
10
12
|
async function initMySQL(config) {
|
|
@@ -2,7 +2,8 @@ import type { MySQLConfig } from './config-loader.js';
|
|
|
2
2
|
import type { ConnectionTestResult, DatabaseListResult, DatabaseUtil, ExplainResult, IndexResult, OutputFormat, QueryResult, TableListResult, TableStructureResult } from './database.js';
|
|
3
3
|
export declare class MySQLUtil implements DatabaseUtil {
|
|
4
4
|
private config;
|
|
5
|
-
private
|
|
5
|
+
private pools;
|
|
6
|
+
private querySlots;
|
|
6
7
|
constructor(config: MySQLConfig);
|
|
7
8
|
closeAll(): Promise<void>;
|
|
8
9
|
describeTable(profileName: string, table: string, format?: 'json' | 'table' | 'toon'): Promise<TableStructureResult>;
|
|
@@ -12,8 +13,11 @@ export declare class MySQLUtil implements DatabaseUtil {
|
|
|
12
13
|
listTables(profileName: string): Promise<TableListResult>;
|
|
13
14
|
showIndexes(profileName: string, table: string, format?: 'json' | 'table' | 'toon'): Promise<IndexResult>;
|
|
14
15
|
testConnection(profileName: string): Promise<ConnectionTestResult>;
|
|
16
|
+
private acquireQuerySlot;
|
|
15
17
|
private formatReadResult;
|
|
16
18
|
private formatRows;
|
|
17
19
|
private formatWriteResult;
|
|
18
|
-
private
|
|
20
|
+
private getPool;
|
|
21
|
+
private getQueryLimit;
|
|
22
|
+
private runQuery;
|
|
19
23
|
}
|
|
@@ -2,25 +2,37 @@ import mysql from 'mysql2/promise';
|
|
|
2
2
|
import { getMySQLConnectionOptions } from './config-loader.js';
|
|
3
3
|
import { FORMATTERS } from './formatters.js';
|
|
4
4
|
import { analyzeQuery, applyDefaultLimit, checkBlacklist, getQueryType, requiresConfirmation } from './query-validator.js';
|
|
5
|
+
const DEFAULT_MAX_CONCURRENT_QUERIES = 5;
|
|
6
|
+
const DEFAULT_QUEUE_TIMEOUT_MS = 60_000;
|
|
5
7
|
export class MySQLUtil {
|
|
6
8
|
config;
|
|
7
|
-
|
|
9
|
+
pools;
|
|
10
|
+
querySlots;
|
|
8
11
|
constructor(config) {
|
|
9
12
|
this.config = config;
|
|
10
|
-
this.
|
|
13
|
+
this.pools = new Map();
|
|
14
|
+
this.querySlots = new Map();
|
|
11
15
|
}
|
|
12
16
|
async closeAll() {
|
|
13
|
-
|
|
14
|
-
this.
|
|
15
|
-
|
|
17
|
+
// Reject queued queries first so nothing waits forever on a closed util.
|
|
18
|
+
for (const slot of this.querySlots.values()) {
|
|
19
|
+
for (const waiter of slot.waiting.splice(0)) {
|
|
20
|
+
waiter.reject(new Error('Connections were closed while the query was waiting for a free slot'));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
this.querySlots.clear();
|
|
24
|
+
const pools = [...this.pools.values()];
|
|
25
|
+
this.pools.clear();
|
|
26
|
+
await Promise.allSettled(pools.map((pool) => pool.end()));
|
|
16
27
|
}
|
|
17
28
|
async describeTable(profileName, table, format = 'table') {
|
|
18
29
|
try {
|
|
19
|
-
const
|
|
20
|
-
const [rows, fields] = await connection.query(`DESCRIBE ${table}`);
|
|
30
|
+
const [rows, fields] = await this.runQuery(profileName, `DESCRIBE ${table}`);
|
|
21
31
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
data: {
|
|
33
|
+
result: this.formatRows(rows, fields, format),
|
|
34
|
+
structure: rows,
|
|
35
|
+
},
|
|
24
36
|
success: true,
|
|
25
37
|
};
|
|
26
38
|
}
|
|
@@ -44,8 +56,10 @@ export class MySQLUtil {
|
|
|
44
56
|
const confirmationCheck = requiresConfirmation(query, this.config.safety.requireConfirmationFor);
|
|
45
57
|
if (confirmationCheck.required) {
|
|
46
58
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
data: {
|
|
60
|
+
message: `${confirmationCheck.message}\nQuery: ${query}`,
|
|
61
|
+
requiresConfirmation: true,
|
|
62
|
+
},
|
|
49
63
|
success: false,
|
|
50
64
|
};
|
|
51
65
|
}
|
|
@@ -68,18 +82,22 @@ export class MySQLUtil {
|
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
84
|
try {
|
|
71
|
-
const
|
|
72
|
-
const [rows, fields] = await connection.query(finalQuery);
|
|
85
|
+
const [rows, fields] = await this.runQuery(profileName, finalQuery);
|
|
73
86
|
const isRead = queryType === 'SELECT' || queryType === 'SHOW' || queryType === 'DESCRIBE' || queryType === 'EXPLAIN';
|
|
74
|
-
|
|
87
|
+
let data = isRead
|
|
75
88
|
? this.formatReadResult(rows, fields, format, notices)
|
|
76
|
-
: this.formatWriteResult(rows, notices);
|
|
89
|
+
: this.formatWriteResult(rows, notices, format);
|
|
90
|
+
if (format === 'json') {
|
|
91
|
+
data = JSON.parse(data);
|
|
92
|
+
}
|
|
77
93
|
const notice = notices.join('\n\n');
|
|
78
94
|
// For human (table) output everything stays on stdout, exactly as before.
|
|
79
95
|
// For machine formats the data is returned alone and notices go to stderr.
|
|
80
96
|
return {
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
data: {
|
|
98
|
+
notices: machineFormat ? notice : undefined,
|
|
99
|
+
result: machineFormat ? data : `${notice}\n\n${data}`,
|
|
100
|
+
},
|
|
83
101
|
success: true,
|
|
84
102
|
};
|
|
85
103
|
}
|
|
@@ -93,11 +111,12 @@ export class MySQLUtil {
|
|
|
93
111
|
}
|
|
94
112
|
async explainQuery(profileName, query, format = 'table') {
|
|
95
113
|
try {
|
|
96
|
-
const
|
|
97
|
-
const [rows, fields] = await connection.query(`EXPLAIN ${query}`);
|
|
114
|
+
const [rows, fields] = await this.runQuery(profileName, `EXPLAIN ${query}`);
|
|
98
115
|
return {
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
data: {
|
|
117
|
+
plan: rows,
|
|
118
|
+
result: this.formatRows(rows, fields, format),
|
|
119
|
+
},
|
|
101
120
|
success: true,
|
|
102
121
|
};
|
|
103
122
|
}
|
|
@@ -111,12 +130,13 @@ export class MySQLUtil {
|
|
|
111
130
|
}
|
|
112
131
|
async listDatabases(profileName) {
|
|
113
132
|
try {
|
|
114
|
-
const
|
|
115
|
-
const [rows] = await connection.query('SHOW DATABASES');
|
|
133
|
+
const [rows] = await this.runQuery(profileName, 'SHOW DATABASES');
|
|
116
134
|
const databases = rows.map((row) => row.Database);
|
|
117
135
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
data: {
|
|
137
|
+
databases,
|
|
138
|
+
result: `Databases:\n${databases.map((db) => ` • ${db}`).join('\n')}`,
|
|
139
|
+
},
|
|
120
140
|
success: true,
|
|
121
141
|
};
|
|
122
142
|
}
|
|
@@ -130,15 +150,16 @@ export class MySQLUtil {
|
|
|
130
150
|
}
|
|
131
151
|
async listTables(profileName) {
|
|
132
152
|
try {
|
|
133
|
-
const
|
|
134
|
-
const [rows] = await connection.query('SHOW TABLES');
|
|
153
|
+
const [rows] = await this.runQuery(profileName, 'SHOW TABLES');
|
|
135
154
|
const rowsArray = rows;
|
|
136
155
|
const tableKey = Object.keys(rowsArray[0])[0];
|
|
137
156
|
const tables = rowsArray.map((row) => row[tableKey]);
|
|
138
157
|
return {
|
|
139
|
-
|
|
158
|
+
data: {
|
|
159
|
+
result: `Tables in database:\n${tables.map((table) => ` • ${table}`).join('\n')}`,
|
|
160
|
+
tables,
|
|
161
|
+
},
|
|
140
162
|
success: true,
|
|
141
|
-
tables,
|
|
142
163
|
};
|
|
143
164
|
}
|
|
144
165
|
catch (error) {
|
|
@@ -151,11 +172,12 @@ export class MySQLUtil {
|
|
|
151
172
|
}
|
|
152
173
|
async showIndexes(profileName, table, format = 'table') {
|
|
153
174
|
try {
|
|
154
|
-
const
|
|
155
|
-
const [rows, fields] = await connection.query(`SHOW INDEXES FROM ${table}`);
|
|
175
|
+
const [rows, fields] = await this.runQuery(profileName, `SHOW INDEXES FROM ${table}`);
|
|
156
176
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
data: {
|
|
178
|
+
indexes: rows,
|
|
179
|
+
result: this.formatRows(rows, fields, format),
|
|
180
|
+
},
|
|
159
181
|
success: true,
|
|
160
182
|
};
|
|
161
183
|
}
|
|
@@ -169,14 +191,15 @@ export class MySQLUtil {
|
|
|
169
191
|
}
|
|
170
192
|
async testConnection(profileName) {
|
|
171
193
|
try {
|
|
172
|
-
const
|
|
173
|
-
const [rows] = await connection.query('SELECT VERSION() as version, DATABASE() as current_database');
|
|
194
|
+
const [rows] = await this.runQuery(profileName, 'SELECT VERSION() as version, DATABASE() as current_database');
|
|
174
195
|
const info = rows[0];
|
|
175
196
|
return {
|
|
176
|
-
|
|
177
|
-
|
|
197
|
+
data: {
|
|
198
|
+
database: info.current_database,
|
|
199
|
+
result: `Connection successful!\n\nProfile: ${profileName}\nMySQL Version: ${info.version}\nCurrent Database: ${info.current_database}`,
|
|
200
|
+
version: info.version,
|
|
201
|
+
},
|
|
178
202
|
success: true,
|
|
179
|
-
version: info.version,
|
|
180
203
|
};
|
|
181
204
|
}
|
|
182
205
|
catch (error) {
|
|
@@ -187,6 +210,56 @@ export class MySQLUtil {
|
|
|
187
210
|
};
|
|
188
211
|
}
|
|
189
212
|
}
|
|
213
|
+
// Grants a query slot for the profile, or waits until one frees up. The
|
|
214
|
+
// returned release callback must be invoked exactly once per acquisition.
|
|
215
|
+
acquireQuerySlot(profileName) {
|
|
216
|
+
const limit = this.getQueryLimit(profileName);
|
|
217
|
+
let slot = this.querySlots.get(profileName);
|
|
218
|
+
if (!slot) {
|
|
219
|
+
slot = { active: 0, waiting: [] };
|
|
220
|
+
this.querySlots.set(profileName, slot);
|
|
221
|
+
}
|
|
222
|
+
const state = slot;
|
|
223
|
+
const release = () => {
|
|
224
|
+
const next = state.waiting.shift();
|
|
225
|
+
if (next) {
|
|
226
|
+
next.grant();
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
state.active -= 1;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
if (state.active < limit) {
|
|
233
|
+
state.active += 1;
|
|
234
|
+
return Promise.resolve(release);
|
|
235
|
+
}
|
|
236
|
+
const timeoutMs = this.config.profiles[profileName]?.queryQueueTimeoutMs ??
|
|
237
|
+
this.config.safety.queryQueueTimeoutMs ??
|
|
238
|
+
DEFAULT_QUEUE_TIMEOUT_MS;
|
|
239
|
+
process.stderr.write(`Waiting for a free query slot (${limit}/${limit} in use for profile "${profileName}")...\n`);
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const waiter = {
|
|
242
|
+
grant() {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
resolve(release);
|
|
245
|
+
},
|
|
246
|
+
reject(error) {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
reject(error);
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
const timer = setTimeout(() => {
|
|
252
|
+
const index = state.waiting.indexOf(waiter);
|
|
253
|
+
if (index !== -1)
|
|
254
|
+
state.waiting.splice(index, 1);
|
|
255
|
+
reject(new Error(`Timed out after ${timeoutMs / 1000}s waiting for a free query slot ` +
|
|
256
|
+
`(limit: ${limit} concurrent queries for profile "${profileName}")`));
|
|
257
|
+
}, timeoutMs);
|
|
258
|
+
// Don't let a pending queue timer keep the CLI process alive.
|
|
259
|
+
timer.unref?.();
|
|
260
|
+
state.waiting.push(waiter);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
190
263
|
formatReadResult(rows, fields, format, notices) {
|
|
191
264
|
const rowCount = Array.isArray(rows) ? rows.length : 0;
|
|
192
265
|
notices.push(`Query executed successfully. Rows returned: ${rowCount}`);
|
|
@@ -195,35 +268,54 @@ export class MySQLUtil {
|
|
|
195
268
|
formatRows(rows, fields, format) {
|
|
196
269
|
return FORMATTERS[format](rows, fields);
|
|
197
270
|
}
|
|
198
|
-
formatWriteResult(rows, notices) {
|
|
271
|
+
formatWriteResult(rows, notices, format) {
|
|
199
272
|
const affectedRows = rows.affectedRows ?? 0;
|
|
200
273
|
const insertId = rows.insertId ?? null;
|
|
201
274
|
notices.push('Query executed successfully.');
|
|
275
|
+
// The caller JSON.parses the result for json output, so emit valid JSON
|
|
276
|
+
// here rather than the human-readable string (which would throw on parse).
|
|
277
|
+
if (format === 'json') {
|
|
278
|
+
const payload = { affectedRows };
|
|
279
|
+
if (insertId)
|
|
280
|
+
payload.insertId = insertId;
|
|
281
|
+
return JSON.stringify(payload, null, 2);
|
|
282
|
+
}
|
|
202
283
|
let data = `Affected rows: ${affectedRows}\n`;
|
|
203
284
|
if (insertId)
|
|
204
285
|
data += `Insert ID: ${insertId}\n`;
|
|
205
286
|
return data;
|
|
206
287
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
288
|
+
// The pool is sized to the profile's query limit so slot holders each get a
|
|
289
|
+
// real physical connection — a single Connection would serialize commands on
|
|
290
|
+
// the wire and make the concurrency limit meaningless.
|
|
291
|
+
getPool(profileName) {
|
|
292
|
+
const existing = this.pools.get(profileName);
|
|
293
|
+
if (existing)
|
|
294
|
+
return existing;
|
|
295
|
+
const pool = mysql.createPool({
|
|
296
|
+
...getMySQLConnectionOptions(this.config, profileName),
|
|
297
|
+
connectionLimit: this.getQueryLimit(profileName),
|
|
298
|
+
waitForConnections: true,
|
|
299
|
+
});
|
|
300
|
+
this.pools.set(profileName, pool);
|
|
301
|
+
return pool;
|
|
302
|
+
}
|
|
303
|
+
getQueryLimit(profileName) {
|
|
304
|
+
const configuredLimit = this.config.profiles[profileName]?.maxConcurrentQueries ??
|
|
305
|
+
this.config.safety.maxConcurrentQueries ??
|
|
306
|
+
DEFAULT_MAX_CONCURRENT_QUERIES;
|
|
307
|
+
// A limit below 1 would leave every query waiting forever.
|
|
308
|
+
return Math.max(1, configuredLimit);
|
|
309
|
+
}
|
|
310
|
+
// All queries go through here so concurrent load on the same profile is
|
|
311
|
+
// capped at maxConcurrentQueries; excess queries wait for a free slot.
|
|
312
|
+
async runQuery(profileName, sql) {
|
|
313
|
+
const release = await this.acquireQuerySlot(profileName);
|
|
221
314
|
try {
|
|
222
|
-
return await
|
|
315
|
+
return (await this.getPool(profileName).query(sql));
|
|
223
316
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
throw error;
|
|
317
|
+
finally {
|
|
318
|
+
release();
|
|
227
319
|
}
|
|
228
320
|
}
|
|
229
321
|
}
|