@hesed/mysql 0.5.2 → 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 +33 -29
- package/dist/commands/mysql/auth/add.js +14 -0
- package/dist/commands/mysql/auth/update.js +14 -0
- package/dist/mysql/config-loader.d.ts +4 -0
- package/dist/mysql/mysql-client.js +2 -0
- package/dist/mysql/mysql-utils.d.ts +6 -2
- package/dist/mysql/mysql-utils.js +102 -37
- package/oclif.manifest.json +33 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ $ npm install -g @hesed/mysql
|
|
|
26
26
|
$ mq COMMAND
|
|
27
27
|
running command...
|
|
28
28
|
$ mq (--version)
|
|
29
|
-
@hesed/mysql/0.
|
|
29
|
+
@hesed/mysql/0.6.0 linux-x64 node-v24.18.0
|
|
30
30
|
$ mq --help [COMMAND]
|
|
31
31
|
USAGE
|
|
32
32
|
$ mq COMMAND
|
|
@@ -57,16 +57,18 @@ Add MySQL authentication
|
|
|
57
57
|
```
|
|
58
58
|
USAGE
|
|
59
59
|
$ mq mysql auth add -p <value> --host <value> --port <value> -u <value> --password <value> -d <value> --ssl
|
|
60
|
-
[--json]
|
|
60
|
+
--maxConcurrentQueries <value> --queryQueueTimeoutMs <value> [--json]
|
|
61
61
|
|
|
62
62
|
FLAGS
|
|
63
|
-
-d, --database=<value>
|
|
64
|
-
-p, --profile=<value>
|
|
65
|
-
-u, --user=<value>
|
|
66
|
-
--host=<value>
|
|
67
|
-
--
|
|
68
|
-
--
|
|
69
|
-
--
|
|
63
|
+
-d, --database=<value> (required) Database name
|
|
64
|
+
-p, --profile=<value> (required) Profile name
|
|
65
|
+
-u, --user=<value> (required) Username
|
|
66
|
+
--host=<value> (required) MySQL host
|
|
67
|
+
--maxConcurrentQueries=<value> (required) Max concurrent queries for this profile
|
|
68
|
+
--password=<value> (required) Password
|
|
69
|
+
--port=<value> (required) MySQL port
|
|
70
|
+
--queryQueueTimeoutMs=<value> (required) Milliseconds a query may wait for a free query slot before failing
|
|
71
|
+
--ssl (required) Use SSL
|
|
70
72
|
|
|
71
73
|
GLOBAL FLAGS
|
|
72
74
|
--json Format output as json.
|
|
@@ -80,7 +82,7 @@ EXAMPLES
|
|
|
80
82
|
$ mq mysql auth add -p prod
|
|
81
83
|
```
|
|
82
84
|
|
|
83
|
-
_See code: [src/commands/mysql/auth/add.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
85
|
+
_See code: [src/commands/mysql/auth/add.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/auth/add.ts)_
|
|
84
86
|
|
|
85
87
|
## `mq mysql auth delete`
|
|
86
88
|
|
|
@@ -105,7 +107,7 @@ EXAMPLES
|
|
|
105
107
|
$ mq mysql auth delete -p prod
|
|
106
108
|
```
|
|
107
109
|
|
|
108
|
-
_See code: [src/commands/mysql/auth/delete.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
110
|
+
_See code: [src/commands/mysql/auth/delete.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/auth/delete.ts)_
|
|
109
111
|
|
|
110
112
|
## `mq mysql auth list`
|
|
111
113
|
|
|
@@ -125,7 +127,7 @@ EXAMPLES
|
|
|
125
127
|
$ mq mysql auth list
|
|
126
128
|
```
|
|
127
129
|
|
|
128
|
-
_See code: [src/commands/mysql/auth/list.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
130
|
+
_See code: [src/commands/mysql/auth/list.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/auth/list.ts)_
|
|
129
131
|
|
|
130
132
|
## `mq mysql auth profile`
|
|
131
133
|
|
|
@@ -150,7 +152,7 @@ EXAMPLES
|
|
|
150
152
|
$ mq mysql auth profile --default test
|
|
151
153
|
```
|
|
152
154
|
|
|
153
|
-
_See code: [src/commands/mysql/auth/profile.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
155
|
+
_See code: [src/commands/mysql/auth/profile.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/auth/profile.ts)_
|
|
154
156
|
|
|
155
157
|
## `mq mysql auth test`
|
|
156
158
|
|
|
@@ -175,7 +177,7 @@ EXAMPLES
|
|
|
175
177
|
$ mq mysql auth test -p prod
|
|
176
178
|
```
|
|
177
179
|
|
|
178
|
-
_See code: [src/commands/mysql/auth/test.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
180
|
+
_See code: [src/commands/mysql/auth/test.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/auth/test.ts)_
|
|
179
181
|
|
|
180
182
|
## `mq mysql auth update`
|
|
181
183
|
|
|
@@ -184,16 +186,18 @@ Update MySQL authentication
|
|
|
184
186
|
```
|
|
185
187
|
USAGE
|
|
186
188
|
$ mq mysql auth update -p <value> --host <value> --port <value> -u <value> --password <value> -d <value> --ssl
|
|
187
|
-
[--json]
|
|
189
|
+
--maxConcurrentQueries <value> --queryQueueTimeoutMs <value> [--json]
|
|
188
190
|
|
|
189
191
|
FLAGS
|
|
190
|
-
-d, --database=<value>
|
|
191
|
-
-p, --profile=<value>
|
|
192
|
-
-u, --user=<value>
|
|
193
|
-
--host=<value>
|
|
194
|
-
--
|
|
195
|
-
--
|
|
196
|
-
--
|
|
192
|
+
-d, --database=<value> (required) Database name
|
|
193
|
+
-p, --profile=<value> (required) Profile name
|
|
194
|
+
-u, --user=<value> (required) Username
|
|
195
|
+
--host=<value> (required) MySQL host
|
|
196
|
+
--maxConcurrentQueries=<value> (required) Max concurrent queries for this profile
|
|
197
|
+
--password=<value> (required) Password
|
|
198
|
+
--port=<value> (required) MySQL port
|
|
199
|
+
--queryQueueTimeoutMs=<value> (required) Milliseconds a query may wait for a free query slot before failing
|
|
200
|
+
--ssl (required) Use SSL
|
|
197
201
|
|
|
198
202
|
GLOBAL FLAGS
|
|
199
203
|
--json Format output as json.
|
|
@@ -207,7 +211,7 @@ EXAMPLES
|
|
|
207
211
|
$ mq mysql auth update -p test
|
|
208
212
|
```
|
|
209
213
|
|
|
210
|
-
_See code: [src/commands/mysql/auth/update.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
214
|
+
_See code: [src/commands/mysql/auth/update.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/auth/update.ts)_
|
|
211
215
|
|
|
212
216
|
## `mq mysql databases`
|
|
213
217
|
|
|
@@ -232,7 +236,7 @@ EXAMPLES
|
|
|
232
236
|
$ mq mysql databases -p staging
|
|
233
237
|
```
|
|
234
238
|
|
|
235
|
-
_See code: [src/commands/mysql/databases.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
239
|
+
_See code: [src/commands/mysql/databases.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/databases.ts)_
|
|
236
240
|
|
|
237
241
|
## `mq mysql describe-table TABLE`
|
|
238
242
|
|
|
@@ -261,7 +265,7 @@ EXAMPLES
|
|
|
261
265
|
$ mq mysql describe-table orders --toon -p prod
|
|
262
266
|
```
|
|
263
267
|
|
|
264
|
-
_See code: [src/commands/mysql/describe-table.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
268
|
+
_See code: [src/commands/mysql/describe-table.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/describe-table.ts)_
|
|
265
269
|
|
|
266
270
|
## `mq mysql explain QUERY`
|
|
267
271
|
|
|
@@ -290,7 +294,7 @@ EXAMPLES
|
|
|
290
294
|
$ mq mysql explain "SELECT * FROM orders JOIN users ON orders.user_id = users.id" --json
|
|
291
295
|
```
|
|
292
296
|
|
|
293
|
-
_See code: [src/commands/mysql/explain.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
297
|
+
_See code: [src/commands/mysql/explain.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/explain.ts)_
|
|
294
298
|
|
|
295
299
|
## `mq mysql indexes TABLE`
|
|
296
300
|
|
|
@@ -319,7 +323,7 @@ EXAMPLES
|
|
|
319
323
|
$ mq mysql indexes orders --json -p prod
|
|
320
324
|
```
|
|
321
325
|
|
|
322
|
-
_See code: [src/commands/mysql/indexes.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
326
|
+
_See code: [src/commands/mysql/indexes.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/indexes.ts)_
|
|
323
327
|
|
|
324
328
|
## `mq mysql query QUERY`
|
|
325
329
|
|
|
@@ -351,7 +355,7 @@ EXAMPLES
|
|
|
351
355
|
$ mq mysql query "DELETE FROM sessions" -p prod --skip-confirmation
|
|
352
356
|
```
|
|
353
357
|
|
|
354
|
-
_See code: [src/commands/mysql/query.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
358
|
+
_See code: [src/commands/mysql/query.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/query.ts)_
|
|
355
359
|
|
|
356
360
|
## `mq mysql tables`
|
|
357
361
|
|
|
@@ -376,5 +380,5 @@ EXAMPLES
|
|
|
376
380
|
$ mq mysql tables -p local
|
|
377
381
|
```
|
|
378
382
|
|
|
379
|
-
_See code: [src/commands/mysql/tables.ts](https://github.com/hesedcasa/mysql/blob/v0.
|
|
383
|
+
_See code: [src/commands/mysql/tables.ts](https://github.com/hesedcasa/mysql/blob/v0.6.0/src/commands/mysql/tables.ts)_
|
|
380
384
|
<!-- commandsstop -->
|
|
@@ -7,6 +7,20 @@ const fields = [
|
|
|
7
7
|
{ description: 'Password', name: 'password', type: 'string' },
|
|
8
8
|
{ char: 'd', description: 'Database name', name: 'database', type: 'string' },
|
|
9
9
|
{ default: false, description: 'Use SSL', name: 'ssl', required: false, type: 'boolean' },
|
|
10
|
+
{
|
|
11
|
+
default: 5,
|
|
12
|
+
description: 'Max concurrent queries for this profile',
|
|
13
|
+
name: 'maxConcurrentQueries',
|
|
14
|
+
required: false,
|
|
15
|
+
type: 'number',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
default: 60_000,
|
|
19
|
+
description: 'Milliseconds a query may wait for a free query slot before failing',
|
|
20
|
+
name: 'queryQueueTimeoutMs',
|
|
21
|
+
required: false,
|
|
22
|
+
type: 'number',
|
|
23
|
+
},
|
|
10
24
|
];
|
|
11
25
|
export default createAuthAddCommand({
|
|
12
26
|
clearClients: closeConnections,
|
|
@@ -7,6 +7,20 @@ const fields = [
|
|
|
7
7
|
{ description: 'Password', name: 'password', type: 'string' },
|
|
8
8
|
{ char: 'd', description: 'Database name', name: 'database', type: 'string' },
|
|
9
9
|
{ default: false, description: 'Use SSL', name: 'ssl', required: false, type: 'boolean' },
|
|
10
|
+
{
|
|
11
|
+
default: 5,
|
|
12
|
+
description: 'Max concurrent queries for this profile',
|
|
13
|
+
name: 'maxConcurrentQueries',
|
|
14
|
+
required: false,
|
|
15
|
+
type: 'number',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
default: 60_000,
|
|
19
|
+
description: 'Milliseconds a query may wait for a free query slot before failing',
|
|
20
|
+
name: 'queryQueueTimeoutMs',
|
|
21
|
+
required: false,
|
|
22
|
+
type: 'number',
|
|
23
|
+
},
|
|
10
24
|
];
|
|
11
25
|
export default createAuthUpdateCommand({
|
|
12
26
|
clearClients: closeConnections,
|
|
@@ -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 {
|
|
@@ -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,22 +2,32 @@ 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
32
|
data: {
|
|
23
33
|
result: this.formatRows(rows, fields, format),
|
|
@@ -72,8 +82,7 @@ export class MySQLUtil {
|
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
84
|
try {
|
|
75
|
-
const
|
|
76
|
-
const [rows, fields] = await connection.query(finalQuery);
|
|
85
|
+
const [rows, fields] = await this.runQuery(profileName, finalQuery);
|
|
77
86
|
const isRead = queryType === 'SELECT' || queryType === 'SHOW' || queryType === 'DESCRIBE' || queryType === 'EXPLAIN';
|
|
78
87
|
let data = isRead
|
|
79
88
|
? this.formatReadResult(rows, fields, format, notices)
|
|
@@ -102,8 +111,7 @@ export class MySQLUtil {
|
|
|
102
111
|
}
|
|
103
112
|
async explainQuery(profileName, query, format = 'table') {
|
|
104
113
|
try {
|
|
105
|
-
const
|
|
106
|
-
const [rows, fields] = await connection.query(`EXPLAIN ${query}`);
|
|
114
|
+
const [rows, fields] = await this.runQuery(profileName, `EXPLAIN ${query}`);
|
|
107
115
|
return {
|
|
108
116
|
data: {
|
|
109
117
|
plan: rows,
|
|
@@ -122,8 +130,7 @@ export class MySQLUtil {
|
|
|
122
130
|
}
|
|
123
131
|
async listDatabases(profileName) {
|
|
124
132
|
try {
|
|
125
|
-
const
|
|
126
|
-
const [rows] = await connection.query('SHOW DATABASES');
|
|
133
|
+
const [rows] = await this.runQuery(profileName, 'SHOW DATABASES');
|
|
127
134
|
const databases = rows.map((row) => row.Database);
|
|
128
135
|
return {
|
|
129
136
|
data: {
|
|
@@ -143,8 +150,7 @@ export class MySQLUtil {
|
|
|
143
150
|
}
|
|
144
151
|
async listTables(profileName) {
|
|
145
152
|
try {
|
|
146
|
-
const
|
|
147
|
-
const [rows] = await connection.query('SHOW TABLES');
|
|
153
|
+
const [rows] = await this.runQuery(profileName, 'SHOW TABLES');
|
|
148
154
|
const rowsArray = rows;
|
|
149
155
|
const tableKey = Object.keys(rowsArray[0])[0];
|
|
150
156
|
const tables = rowsArray.map((row) => row[tableKey]);
|
|
@@ -166,8 +172,7 @@ export class MySQLUtil {
|
|
|
166
172
|
}
|
|
167
173
|
async showIndexes(profileName, table, format = 'table') {
|
|
168
174
|
try {
|
|
169
|
-
const
|
|
170
|
-
const [rows, fields] = await connection.query(`SHOW INDEXES FROM ${table}`);
|
|
175
|
+
const [rows, fields] = await this.runQuery(profileName, `SHOW INDEXES FROM ${table}`);
|
|
171
176
|
return {
|
|
172
177
|
data: {
|
|
173
178
|
indexes: rows,
|
|
@@ -186,8 +191,7 @@ export class MySQLUtil {
|
|
|
186
191
|
}
|
|
187
192
|
async testConnection(profileName) {
|
|
188
193
|
try {
|
|
189
|
-
const
|
|
190
|
-
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');
|
|
191
195
|
const info = rows[0];
|
|
192
196
|
return {
|
|
193
197
|
data: {
|
|
@@ -206,6 +210,56 @@ export class MySQLUtil {
|
|
|
206
210
|
};
|
|
207
211
|
}
|
|
208
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
|
+
}
|
|
209
263
|
formatReadResult(rows, fields, format, notices) {
|
|
210
264
|
const rowCount = Array.isArray(rows) ? rows.length : 0;
|
|
211
265
|
notices.push(`Query executed successfully. Rows returned: ${rowCount}`);
|
|
@@ -231,26 +285,37 @@ export class MySQLUtil {
|
|
|
231
285
|
data += `Insert ID: ${insertId}\n`;
|
|
232
286
|
return data;
|
|
233
287
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
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);
|
|
248
314
|
try {
|
|
249
|
-
return await
|
|
315
|
+
return (await this.getPool(profileName).query(sql));
|
|
250
316
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
throw error;
|
|
317
|
+
finally {
|
|
318
|
+
release();
|
|
254
319
|
}
|
|
255
320
|
}
|
|
256
321
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -384,6 +384,22 @@
|
|
|
384
384
|
"required": true,
|
|
385
385
|
"allowNo": false,
|
|
386
386
|
"type": "boolean"
|
|
387
|
+
},
|
|
388
|
+
"maxConcurrentQueries": {
|
|
389
|
+
"description": "Max concurrent queries for this profile",
|
|
390
|
+
"name": "maxConcurrentQueries",
|
|
391
|
+
"required": true,
|
|
392
|
+
"hasDynamicHelp": false,
|
|
393
|
+
"multiple": false,
|
|
394
|
+
"type": "option"
|
|
395
|
+
},
|
|
396
|
+
"queryQueueTimeoutMs": {
|
|
397
|
+
"description": "Milliseconds a query may wait for a free query slot before failing",
|
|
398
|
+
"name": "queryQueueTimeoutMs",
|
|
399
|
+
"required": true,
|
|
400
|
+
"hasDynamicHelp": false,
|
|
401
|
+
"multiple": false,
|
|
402
|
+
"type": "option"
|
|
387
403
|
}
|
|
388
404
|
},
|
|
389
405
|
"hasDynamicHelp": false,
|
|
@@ -637,6 +653,22 @@
|
|
|
637
653
|
"required": true,
|
|
638
654
|
"allowNo": false,
|
|
639
655
|
"type": "boolean"
|
|
656
|
+
},
|
|
657
|
+
"maxConcurrentQueries": {
|
|
658
|
+
"description": "Max concurrent queries for this profile",
|
|
659
|
+
"name": "maxConcurrentQueries",
|
|
660
|
+
"required": true,
|
|
661
|
+
"hasDynamicHelp": false,
|
|
662
|
+
"multiple": false,
|
|
663
|
+
"type": "option"
|
|
664
|
+
},
|
|
665
|
+
"queryQueueTimeoutMs": {
|
|
666
|
+
"description": "Milliseconds a query may wait for a free query slot before failing",
|
|
667
|
+
"name": "queryQueueTimeoutMs",
|
|
668
|
+
"required": true,
|
|
669
|
+
"hasDynamicHelp": false,
|
|
670
|
+
"multiple": false,
|
|
671
|
+
"type": "option"
|
|
640
672
|
}
|
|
641
673
|
},
|
|
642
674
|
"hasDynamicHelp": false,
|
|
@@ -657,5 +689,5 @@
|
|
|
657
689
|
]
|
|
658
690
|
}
|
|
659
691
|
},
|
|
660
|
-
"version": "0.
|
|
692
|
+
"version": "0.6.0"
|
|
661
693
|
}
|