@hesed/mysql 0.5.2 → 0.6.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/README.md +33 -29
- package/dist/commands/mysql/auth/add.js +14 -0
- package/dist/commands/mysql/auth/update.js +14 -0
- package/dist/commands/mysql/describe-table.d.ts +1 -1
- package/dist/commands/mysql/explain.d.ts +1 -1
- package/dist/commands/mysql/indexes.d.ts +1 -1
- package/dist/commands/mysql/query.d.ts +1 -1
- package/dist/mysql/config-loader.d.ts +10 -6
- package/dist/mysql/database.d.ts +19 -19
- package/dist/mysql/formatters.js +1 -0
- package/dist/mysql/mysql-client.js +7 -4
- package/dist/mysql/mysql-utils.d.ts +8 -4
- package/dist/mysql/mysql-utils.js +112 -41
- package/dist/mysql/query-validator.d.ts +6 -6
- package/dist/mysql/query-validator.js +1 -1
- package/oclif.manifest.json +33 -1
- package/package.json +8 -7
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.1 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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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.1/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,
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import type { ConnectionOptions as MySQL2ConnectionOptions } from 'mysql2/promise';
|
|
2
|
-
export
|
|
2
|
+
export type 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
|
-
}
|
|
10
|
-
|
|
11
|
+
};
|
|
12
|
+
type SafetyConfig = {
|
|
11
13
|
blacklistedOperations: string[];
|
|
12
14
|
defaultLimit: number;
|
|
15
|
+
maxConcurrentQueries?: number;
|
|
16
|
+
queryQueueTimeoutMs?: number;
|
|
13
17
|
requireConfirmationFor: string[];
|
|
14
|
-
}
|
|
15
|
-
export
|
|
18
|
+
};
|
|
19
|
+
export type MySQLConfig = {
|
|
16
20
|
defaultFormat: 'csv' | 'json' | 'table' | 'toon';
|
|
17
21
|
defaultProfile: string;
|
|
18
22
|
profiles: Record<string, DatabaseProfile>;
|
|
19
23
|
safety: SafetyConfig;
|
|
20
|
-
}
|
|
24
|
+
};
|
|
21
25
|
type MySQLConnectionOptions = Pick<MySQL2ConnectionOptions, 'connectTimeout' | 'database' | 'host' | 'multipleStatements' | 'password' | 'port' | 'ssl' | 'user'>;
|
|
22
26
|
export declare function getMySQLConnectionOptions(config: MySQLConfig, profileName: string): MySQLConnectionOptions;
|
|
23
27
|
export {};
|
package/dist/mysql/database.d.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import type { ApiResult } from '@hesed/plugin-lib';
|
|
2
2
|
export type OutputFormat = 'csv' | 'json' | 'table' | 'toon';
|
|
3
|
-
export
|
|
3
|
+
export type QueryData = {
|
|
4
4
|
message?: string;
|
|
5
5
|
notices?: string;
|
|
6
6
|
requiresConfirmation?: boolean;
|
|
7
7
|
result?: unknown;
|
|
8
|
-
}
|
|
9
|
-
export
|
|
8
|
+
};
|
|
9
|
+
export type DatabaseListData = {
|
|
10
10
|
databases: string[];
|
|
11
11
|
result?: string;
|
|
12
|
-
}
|
|
13
|
-
export
|
|
12
|
+
};
|
|
13
|
+
export type TableListData = {
|
|
14
14
|
result?: string;
|
|
15
15
|
tables: string[];
|
|
16
|
-
}
|
|
17
|
-
export
|
|
16
|
+
};
|
|
17
|
+
export type TableStructureData = {
|
|
18
18
|
result?: string;
|
|
19
|
-
structure: Record<string, unknown
|
|
20
|
-
}
|
|
21
|
-
export
|
|
22
|
-
indexes: Record<string, unknown
|
|
19
|
+
structure: Array<Record<string, unknown>>;
|
|
20
|
+
};
|
|
21
|
+
export type IndexData = {
|
|
22
|
+
indexes: Array<Record<string, unknown>>;
|
|
23
23
|
result?: string;
|
|
24
|
-
}
|
|
25
|
-
export
|
|
26
|
-
plan: Record<string, unknown
|
|
24
|
+
};
|
|
25
|
+
export type ExplainData = {
|
|
26
|
+
plan: Array<Record<string, unknown>>;
|
|
27
27
|
result?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
28
|
+
};
|
|
29
|
+
type ConnectionTestData = {
|
|
30
30
|
database: string;
|
|
31
31
|
result?: string;
|
|
32
32
|
version: string;
|
|
33
|
-
}
|
|
33
|
+
};
|
|
34
34
|
export type QueryResult = ApiResult & {
|
|
35
35
|
data?: QueryData;
|
|
36
36
|
};
|
|
@@ -52,7 +52,7 @@ export type ExplainResult = ApiResult & {
|
|
|
52
52
|
export type ConnectionTestResult = ApiResult & {
|
|
53
53
|
data?: ConnectionTestData;
|
|
54
54
|
};
|
|
55
|
-
export
|
|
55
|
+
export type DatabaseUtil = {
|
|
56
56
|
closeAll(): Promise<void>;
|
|
57
57
|
describeTable(profileName: string, table: string, format?: OutputFormat): Promise<TableStructureResult>;
|
|
58
58
|
executeQuery(profileName: string, query: string, format?: OutputFormat, skipConfirmation?: boolean): Promise<QueryResult>;
|
|
@@ -60,5 +60,5 @@ export interface DatabaseUtil {
|
|
|
60
60
|
listDatabases(profileName: string): Promise<DatabaseListResult>;
|
|
61
61
|
listTables(profileName: string): Promise<TableListResult>;
|
|
62
62
|
showIndexes(profileName: string, table: string, format?: OutputFormat): Promise<IndexResult>;
|
|
63
|
-
}
|
|
63
|
+
};
|
|
64
64
|
export {};
|
package/dist/mysql/formatters.js
CHANGED
|
@@ -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) {
|
|
@@ -66,9 +68,10 @@ export async function testDirectConnection(profile) {
|
|
|
66
68
|
return result;
|
|
67
69
|
}
|
|
68
70
|
export async function closeConnections() {
|
|
69
|
-
if (mysqlUtil) {
|
|
70
|
-
|
|
71
|
-
mysqlUtil = null;
|
|
72
|
-
cachedConfig = null;
|
|
71
|
+
if (!mysqlUtil) {
|
|
72
|
+
return;
|
|
73
73
|
}
|
|
74
|
+
await mysqlUtil.closeAll();
|
|
75
|
+
mysqlUtil = null;
|
|
76
|
+
cachedConfig = null;
|
|
74
77
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { MySQLConfig } from './config-loader.js';
|
|
2
1
|
import type { ConnectionTestResult, DatabaseListResult, DatabaseUtil, ExplainResult, IndexResult, OutputFormat, QueryResult, TableListResult, TableStructureResult } from './database.js';
|
|
2
|
+
import { type MySQLConfig } from './config-loader.js';
|
|
3
3
|
export declare class MySQLUtil implements DatabaseUtil {
|
|
4
|
-
private config;
|
|
5
|
-
private
|
|
4
|
+
private readonly config;
|
|
5
|
+
private readonly pools;
|
|
6
|
+
private readonly 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,38 @@ 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;
|
|
7
|
+
// Formats that must emit only the data payload on stdout.
|
|
8
|
+
const MACHINE_FORMATS = new Set(['csv', 'json', 'toon']);
|
|
9
|
+
// Query types whose result set is rows rather than an OkPacket.
|
|
10
|
+
const READ_QUERY_TYPES = new Set(['DESCRIBE', 'EXPLAIN', 'SELECT', 'SHOW']);
|
|
5
11
|
export class MySQLUtil {
|
|
6
12
|
config;
|
|
7
|
-
|
|
13
|
+
pools;
|
|
14
|
+
querySlots;
|
|
8
15
|
constructor(config) {
|
|
9
16
|
this.config = config;
|
|
10
|
-
this.
|
|
17
|
+
this.pools = new Map();
|
|
18
|
+
this.querySlots = new Map();
|
|
11
19
|
}
|
|
12
20
|
async closeAll() {
|
|
13
|
-
|
|
14
|
-
this.
|
|
15
|
-
|
|
21
|
+
// Reject queued queries first so nothing waits forever on a closed util.
|
|
22
|
+
for (const slot of this.querySlots.values()) {
|
|
23
|
+
const { waiting } = slot;
|
|
24
|
+
slot.waiting = [];
|
|
25
|
+
for (const waiter of waiting) {
|
|
26
|
+
waiter.reject(new Error('Connections were closed while the query was waiting for a free slot'));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this.querySlots.clear();
|
|
30
|
+
const pools = [...this.pools.values()];
|
|
31
|
+
this.pools.clear();
|
|
32
|
+
await Promise.allSettled(pools.map(async (pool) => pool.end()));
|
|
16
33
|
}
|
|
17
34
|
async describeTable(profileName, table, format = 'table') {
|
|
18
35
|
try {
|
|
19
|
-
const
|
|
20
|
-
const [rows, fields] = await connection.query(`DESCRIBE ${table}`);
|
|
36
|
+
const [rows, fields] = await this.runQuery(profileName, `DESCRIBE ${table}`);
|
|
21
37
|
return {
|
|
22
38
|
data: {
|
|
23
39
|
result: this.formatRows(rows, fields, format),
|
|
@@ -56,7 +72,7 @@ export class MySQLUtil {
|
|
|
56
72
|
}
|
|
57
73
|
// Machine-readable formats must emit only the data payload on stdout, so
|
|
58
74
|
// analysis warnings and status lines are collected as notices instead.
|
|
59
|
-
const
|
|
75
|
+
const isMachineFormat = MACHINE_FORMATS.has(format);
|
|
60
76
|
const notices = [];
|
|
61
77
|
const warnings = analyzeQuery(query);
|
|
62
78
|
if (warnings.length > 0) {
|
|
@@ -72,9 +88,8 @@ export class MySQLUtil {
|
|
|
72
88
|
}
|
|
73
89
|
}
|
|
74
90
|
try {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const isRead = queryType === 'SELECT' || queryType === 'SHOW' || queryType === 'DESCRIBE' || queryType === 'EXPLAIN';
|
|
91
|
+
const [rows, fields] = await this.runQuery(profileName, finalQuery);
|
|
92
|
+
const isRead = READ_QUERY_TYPES.has(queryType);
|
|
78
93
|
let data = isRead
|
|
79
94
|
? this.formatReadResult(rows, fields, format, notices)
|
|
80
95
|
: this.formatWriteResult(rows, notices, format);
|
|
@@ -86,8 +101,8 @@ export class MySQLUtil {
|
|
|
86
101
|
// For machine formats the data is returned alone and notices go to stderr.
|
|
87
102
|
return {
|
|
88
103
|
data: {
|
|
89
|
-
notices:
|
|
90
|
-
result:
|
|
104
|
+
notices: isMachineFormat ? notice : undefined,
|
|
105
|
+
result: isMachineFormat ? data : `${notice}\n\n${data}`,
|
|
91
106
|
},
|
|
92
107
|
success: true,
|
|
93
108
|
};
|
|
@@ -102,8 +117,7 @@ export class MySQLUtil {
|
|
|
102
117
|
}
|
|
103
118
|
async explainQuery(profileName, query, format = 'table') {
|
|
104
119
|
try {
|
|
105
|
-
const
|
|
106
|
-
const [rows, fields] = await connection.query(`EXPLAIN ${query}`);
|
|
120
|
+
const [rows, fields] = await this.runQuery(profileName, `EXPLAIN ${query}`);
|
|
107
121
|
return {
|
|
108
122
|
data: {
|
|
109
123
|
plan: rows,
|
|
@@ -122,8 +136,7 @@ export class MySQLUtil {
|
|
|
122
136
|
}
|
|
123
137
|
async listDatabases(profileName) {
|
|
124
138
|
try {
|
|
125
|
-
const
|
|
126
|
-
const [rows] = await connection.query('SHOW DATABASES');
|
|
139
|
+
const [rows] = await this.runQuery(profileName, 'SHOW DATABASES');
|
|
127
140
|
const databases = rows.map((row) => row.Database);
|
|
128
141
|
return {
|
|
129
142
|
data: {
|
|
@@ -143,8 +156,7 @@ export class MySQLUtil {
|
|
|
143
156
|
}
|
|
144
157
|
async listTables(profileName) {
|
|
145
158
|
try {
|
|
146
|
-
const
|
|
147
|
-
const [rows] = await connection.query('SHOW TABLES');
|
|
159
|
+
const [rows] = await this.runQuery(profileName, 'SHOW TABLES');
|
|
148
160
|
const rowsArray = rows;
|
|
149
161
|
const tableKey = Object.keys(rowsArray[0])[0];
|
|
150
162
|
const tables = rowsArray.map((row) => row[tableKey]);
|
|
@@ -166,8 +178,7 @@ export class MySQLUtil {
|
|
|
166
178
|
}
|
|
167
179
|
async showIndexes(profileName, table, format = 'table') {
|
|
168
180
|
try {
|
|
169
|
-
const
|
|
170
|
-
const [rows, fields] = await connection.query(`SHOW INDEXES FROM ${table}`);
|
|
181
|
+
const [rows, fields] = await this.runQuery(profileName, `SHOW INDEXES FROM ${table}`);
|
|
171
182
|
return {
|
|
172
183
|
data: {
|
|
173
184
|
indexes: rows,
|
|
@@ -186,8 +197,7 @@ export class MySQLUtil {
|
|
|
186
197
|
}
|
|
187
198
|
async testConnection(profileName) {
|
|
188
199
|
try {
|
|
189
|
-
const
|
|
190
|
-
const [rows] = await connection.query('SELECT VERSION() as version, DATABASE() as current_database');
|
|
200
|
+
const [rows] = await this.runQuery(profileName, 'SELECT VERSION() as version, DATABASE() as current_database');
|
|
191
201
|
const info = rows[0];
|
|
192
202
|
return {
|
|
193
203
|
data: {
|
|
@@ -206,6 +216,56 @@ export class MySQLUtil {
|
|
|
206
216
|
};
|
|
207
217
|
}
|
|
208
218
|
}
|
|
219
|
+
// Grants a query slot for the profile, or waits until one frees up. The
|
|
220
|
+
// returned release callback must be invoked exactly once per acquisition.
|
|
221
|
+
async acquireQuerySlot(profileName) {
|
|
222
|
+
const limit = this.getQueryLimit(profileName);
|
|
223
|
+
let slot = this.querySlots.get(profileName);
|
|
224
|
+
if (!slot) {
|
|
225
|
+
slot = { active: 0, waiting: [] };
|
|
226
|
+
this.querySlots.set(profileName, slot);
|
|
227
|
+
}
|
|
228
|
+
const state = slot;
|
|
229
|
+
const release = () => {
|
|
230
|
+
const next = state.waiting.shift();
|
|
231
|
+
if (next) {
|
|
232
|
+
next.grant();
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
state.active -= 1;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
if (state.active < limit) {
|
|
239
|
+
state.active += 1;
|
|
240
|
+
return release;
|
|
241
|
+
}
|
|
242
|
+
const timeoutMs = this.config.profiles[profileName]?.queryQueueTimeoutMs ??
|
|
243
|
+
this.config.safety.queryQueueTimeoutMs ??
|
|
244
|
+
DEFAULT_QUEUE_TIMEOUT_MS;
|
|
245
|
+
process.stderr.write(`Waiting for a free query slot (${limit}/${limit} in use for profile "${profileName}")...\n`);
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
const waiter = {
|
|
248
|
+
grant() {
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
resolve(release);
|
|
251
|
+
},
|
|
252
|
+
reject(error) {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
reject(error);
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
const timer = setTimeout(() => {
|
|
258
|
+
const index = state.waiting.indexOf(waiter);
|
|
259
|
+
if (index !== -1)
|
|
260
|
+
state.waiting.splice(index, 1);
|
|
261
|
+
reject(new Error(`Timed out after ${timeoutMs / 1000}s waiting for a free query slot ` +
|
|
262
|
+
`(limit: ${limit} concurrent queries for profile "${profileName}")`));
|
|
263
|
+
}, timeoutMs);
|
|
264
|
+
// Don't let a pending queue timer keep the CLI process alive.
|
|
265
|
+
timer.unref?.();
|
|
266
|
+
state.waiting.push(waiter);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
209
269
|
formatReadResult(rows, fields, format, notices) {
|
|
210
270
|
const rowCount = Array.isArray(rows) ? rows.length : 0;
|
|
211
271
|
notices.push(`Query executed successfully. Rows returned: ${rowCount}`);
|
|
@@ -231,26 +291,37 @@ export class MySQLUtil {
|
|
|
231
291
|
data += `Insert ID: ${insertId}\n`;
|
|
232
292
|
return data;
|
|
233
293
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
294
|
+
// The pool is sized to the profile's query limit so slot holders each get a
|
|
295
|
+
// real physical connection — a single Connection would serialize commands on
|
|
296
|
+
// the wire and make the concurrency limit meaningless.
|
|
297
|
+
getPool(profileName) {
|
|
298
|
+
const existing = this.pools.get(profileName);
|
|
299
|
+
if (existing)
|
|
300
|
+
return existing;
|
|
301
|
+
const pool = mysql.createPool({
|
|
302
|
+
...getMySQLConnectionOptions(this.config, profileName),
|
|
303
|
+
connectionLimit: this.getQueryLimit(profileName),
|
|
304
|
+
waitForConnections: true,
|
|
305
|
+
});
|
|
306
|
+
this.pools.set(profileName, pool);
|
|
307
|
+
return pool;
|
|
308
|
+
}
|
|
309
|
+
getQueryLimit(profileName) {
|
|
310
|
+
const configuredLimit = this.config.profiles[profileName]?.maxConcurrentQueries ??
|
|
311
|
+
this.config.safety.maxConcurrentQueries ??
|
|
312
|
+
DEFAULT_MAX_CONCURRENT_QUERIES;
|
|
313
|
+
// A limit below 1 would leave every query waiting forever.
|
|
314
|
+
return Math.max(1, configuredLimit);
|
|
315
|
+
}
|
|
316
|
+
// All queries go through here so concurrent load on the same profile is
|
|
317
|
+
// capped at maxConcurrentQueries; excess queries wait for a free slot.
|
|
318
|
+
async runQuery(profileName, sql) {
|
|
319
|
+
const release = await this.acquireQuerySlot(profileName);
|
|
248
320
|
try {
|
|
249
|
-
return await
|
|
321
|
+
return await this.getPool(profileName).query(sql);
|
|
250
322
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
throw error;
|
|
323
|
+
finally {
|
|
324
|
+
release();
|
|
254
325
|
}
|
|
255
326
|
}
|
|
256
327
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
type BlacklistCheckResult = {
|
|
2
2
|
allowed: boolean;
|
|
3
3
|
reason?: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
4
|
+
};
|
|
5
|
+
type ConfirmationCheckResult = {
|
|
6
6
|
message?: string;
|
|
7
7
|
required: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
8
|
+
};
|
|
9
|
+
type QueryWarning = {
|
|
10
10
|
level: 'info' | 'warning';
|
|
11
11
|
message: string;
|
|
12
12
|
suggestion: string;
|
|
13
|
-
}
|
|
13
|
+
};
|
|
14
14
|
export declare function checkBlacklist(query: string, blacklistedOperations: string[]): BlacklistCheckResult;
|
|
15
15
|
export declare function requiresConfirmation(query: string, confirmationOperations: string[]): ConfirmationCheckResult;
|
|
16
16
|
export declare function getQueryType(query: string): string;
|
|
@@ -26,7 +26,7 @@ export function requiresConfirmation(query, confirmationOperations) {
|
|
|
26
26
|
}
|
|
27
27
|
export function getQueryType(query) {
|
|
28
28
|
const normalizedQuery = query.trim().toUpperCase();
|
|
29
|
-
const firstWord = normalizedQuery.split(/\s
|
|
29
|
+
const firstWord = normalizedQuery.split(/\s+/, 1)[0];
|
|
30
30
|
const knownTypes = [
|
|
31
31
|
'SELECT',
|
|
32
32
|
'INSERT',
|
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.1"
|
|
661
693
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hesed/mysql",
|
|
3
3
|
"description": "CLI for MySQL database interaction",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"author": "Hesed",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mq": "./bin/run.js"
|
|
8
8
|
},
|
|
9
9
|
"bugs": "https://github.com/hesedcasa/mysql/issues",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@hesed/plugin-lib": "^0.
|
|
11
|
+
"@hesed/plugin-lib": "^0.12.1",
|
|
12
12
|
"@inquirer/prompts": "^8.3.0",
|
|
13
13
|
"@oclif/core": "^4",
|
|
14
14
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"@types/fs-extra": "^11.0.4",
|
|
24
24
|
"@types/mocha": "^10",
|
|
25
25
|
"@types/node": "^26",
|
|
26
|
-
"@types/sinon": "^
|
|
26
|
+
"@types/sinon": "^22.0.0",
|
|
27
27
|
"c8": "^11.0.0",
|
|
28
28
|
"chai": "^6",
|
|
29
|
-
"eslint": "^
|
|
30
|
-
"eslint-config-oclif": "^
|
|
29
|
+
"eslint": "^10",
|
|
30
|
+
"eslint-config-oclif": "^7",
|
|
31
31
|
"eslint-config-prettier": "^10",
|
|
32
32
|
"esmock": "^2.7.3",
|
|
33
33
|
"mocha": "^11",
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
"sinon": "^22.0.0",
|
|
38
38
|
"ts-node": "^10",
|
|
39
39
|
"ts-prune": "^0.10.3",
|
|
40
|
-
"typescript": "^5"
|
|
40
|
+
"typescript": "^5",
|
|
41
|
+
"typescript-eslint": "^8.67.0"
|
|
41
42
|
},
|
|
42
43
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
44
|
+
"node": ">=22.13.0"
|
|
44
45
|
},
|
|
45
46
|
"files": [
|
|
46
47
|
"./bin",
|