@hesed/mysql 0.6.1 → 0.6.2
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 +13 -13
- package/dist/mysql/mysql-utils.js +15 -7
- package/dist/mysql/query-validator.js +162 -14
- package/oclif.manifest.json +1 -1
- package/package.json +9 -5
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.6.
|
|
29
|
+
@hesed/mysql/0.6.2 linux-x64 node-v24.20.0
|
|
30
30
|
$ mq --help [COMMAND]
|
|
31
31
|
USAGE
|
|
32
32
|
$ mq COMMAND
|
|
@@ -82,7 +82,7 @@ EXAMPLES
|
|
|
82
82
|
$ mq mysql auth add -p prod
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
_See code: [src/commands/mysql/auth/add.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
85
|
+
_See code: [src/commands/mysql/auth/add.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/auth/add.ts)_
|
|
86
86
|
|
|
87
87
|
## `mq mysql auth delete`
|
|
88
88
|
|
|
@@ -107,7 +107,7 @@ EXAMPLES
|
|
|
107
107
|
$ mq mysql auth delete -p prod
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
_See code: [src/commands/mysql/auth/delete.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
110
|
+
_See code: [src/commands/mysql/auth/delete.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/auth/delete.ts)_
|
|
111
111
|
|
|
112
112
|
## `mq mysql auth list`
|
|
113
113
|
|
|
@@ -127,7 +127,7 @@ EXAMPLES
|
|
|
127
127
|
$ mq mysql auth list
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
_See code: [src/commands/mysql/auth/list.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
130
|
+
_See code: [src/commands/mysql/auth/list.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/auth/list.ts)_
|
|
131
131
|
|
|
132
132
|
## `mq mysql auth profile`
|
|
133
133
|
|
|
@@ -152,7 +152,7 @@ EXAMPLES
|
|
|
152
152
|
$ mq mysql auth profile --default test
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
_See code: [src/commands/mysql/auth/profile.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
155
|
+
_See code: [src/commands/mysql/auth/profile.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/auth/profile.ts)_
|
|
156
156
|
|
|
157
157
|
## `mq mysql auth test`
|
|
158
158
|
|
|
@@ -177,7 +177,7 @@ EXAMPLES
|
|
|
177
177
|
$ mq mysql auth test -p prod
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
_See code: [src/commands/mysql/auth/test.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
180
|
+
_See code: [src/commands/mysql/auth/test.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/auth/test.ts)_
|
|
181
181
|
|
|
182
182
|
## `mq mysql auth update`
|
|
183
183
|
|
|
@@ -211,7 +211,7 @@ EXAMPLES
|
|
|
211
211
|
$ mq mysql auth update -p test
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
_See code: [src/commands/mysql/auth/update.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
214
|
+
_See code: [src/commands/mysql/auth/update.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/auth/update.ts)_
|
|
215
215
|
|
|
216
216
|
## `mq mysql databases`
|
|
217
217
|
|
|
@@ -236,7 +236,7 @@ EXAMPLES
|
|
|
236
236
|
$ mq mysql databases -p staging
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
_See code: [src/commands/mysql/databases.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
239
|
+
_See code: [src/commands/mysql/databases.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/databases.ts)_
|
|
240
240
|
|
|
241
241
|
## `mq mysql describe-table TABLE`
|
|
242
242
|
|
|
@@ -265,7 +265,7 @@ EXAMPLES
|
|
|
265
265
|
$ mq mysql describe-table orders --toon -p prod
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
-
_See code: [src/commands/mysql/describe-table.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
268
|
+
_See code: [src/commands/mysql/describe-table.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/describe-table.ts)_
|
|
269
269
|
|
|
270
270
|
## `mq mysql explain QUERY`
|
|
271
271
|
|
|
@@ -294,7 +294,7 @@ EXAMPLES
|
|
|
294
294
|
$ mq mysql explain "SELECT * FROM orders JOIN users ON orders.user_id = users.id" --json
|
|
295
295
|
```
|
|
296
296
|
|
|
297
|
-
_See code: [src/commands/mysql/explain.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
297
|
+
_See code: [src/commands/mysql/explain.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/explain.ts)_
|
|
298
298
|
|
|
299
299
|
## `mq mysql indexes TABLE`
|
|
300
300
|
|
|
@@ -323,7 +323,7 @@ EXAMPLES
|
|
|
323
323
|
$ mq mysql indexes orders --json -p prod
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
-
_See code: [src/commands/mysql/indexes.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
326
|
+
_See code: [src/commands/mysql/indexes.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/indexes.ts)_
|
|
327
327
|
|
|
328
328
|
## `mq mysql query QUERY`
|
|
329
329
|
|
|
@@ -355,7 +355,7 @@ EXAMPLES
|
|
|
355
355
|
$ mq mysql query "DELETE FROM sessions" -p prod --skip-confirmation
|
|
356
356
|
```
|
|
357
357
|
|
|
358
|
-
_See code: [src/commands/mysql/query.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
358
|
+
_See code: [src/commands/mysql/query.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/query.ts)_
|
|
359
359
|
|
|
360
360
|
## `mq mysql tables`
|
|
361
361
|
|
|
@@ -380,5 +380,5 @@ EXAMPLES
|
|
|
380
380
|
$ mq mysql tables -p local
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
_See code: [src/commands/mysql/tables.ts](https://github.com/hesedcasa/mysql/blob/v0.6.
|
|
383
|
+
_See code: [src/commands/mysql/tables.ts](https://github.com/hesedcasa/mysql/blob/v0.6.2/src/commands/mysql/tables.ts)_
|
|
384
384
|
<!-- commandsstop -->
|
|
@@ -45,7 +45,7 @@ export class MySQLUtil {
|
|
|
45
45
|
catch (error) {
|
|
46
46
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
47
47
|
return {
|
|
48
|
-
error:
|
|
48
|
+
error: errorMessage,
|
|
49
49
|
success: false,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
@@ -110,7 +110,7 @@ export class MySQLUtil {
|
|
|
110
110
|
catch (error) {
|
|
111
111
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
112
112
|
return {
|
|
113
|
-
error:
|
|
113
|
+
error: errorMessage,
|
|
114
114
|
success: false,
|
|
115
115
|
};
|
|
116
116
|
}
|
|
@@ -129,7 +129,7 @@ export class MySQLUtil {
|
|
|
129
129
|
catch (error) {
|
|
130
130
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
131
131
|
return {
|
|
132
|
-
error:
|
|
132
|
+
error: errorMessage,
|
|
133
133
|
success: false,
|
|
134
134
|
};
|
|
135
135
|
}
|
|
@@ -149,7 +149,7 @@ export class MySQLUtil {
|
|
|
149
149
|
catch (error) {
|
|
150
150
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
151
151
|
return {
|
|
152
|
-
error:
|
|
152
|
+
error: errorMessage,
|
|
153
153
|
success: false,
|
|
154
154
|
};
|
|
155
155
|
}
|
|
@@ -158,6 +158,14 @@ export class MySQLUtil {
|
|
|
158
158
|
try {
|
|
159
159
|
const [rows] = await this.runQuery(profileName, 'SHOW TABLES');
|
|
160
160
|
const rowsArray = rows;
|
|
161
|
+
// SHOW TABLES names its one column after the database, so the key has to
|
|
162
|
+
// be read off a row — an empty schema has none.
|
|
163
|
+
if (rowsArray.length === 0) {
|
|
164
|
+
return {
|
|
165
|
+
data: { result: 'No tables found in this database', tables: [] },
|
|
166
|
+
success: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
161
169
|
const tableKey = Object.keys(rowsArray[0])[0];
|
|
162
170
|
const tables = rowsArray.map((row) => row[tableKey]);
|
|
163
171
|
return {
|
|
@@ -171,7 +179,7 @@ export class MySQLUtil {
|
|
|
171
179
|
catch (error) {
|
|
172
180
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
173
181
|
return {
|
|
174
|
-
error:
|
|
182
|
+
error: errorMessage,
|
|
175
183
|
success: false,
|
|
176
184
|
};
|
|
177
185
|
}
|
|
@@ -190,7 +198,7 @@ export class MySQLUtil {
|
|
|
190
198
|
catch (error) {
|
|
191
199
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
192
200
|
return {
|
|
193
|
-
error:
|
|
201
|
+
error: errorMessage,
|
|
194
202
|
success: false,
|
|
195
203
|
};
|
|
196
204
|
}
|
|
@@ -211,7 +219,7 @@ export class MySQLUtil {
|
|
|
211
219
|
catch (error) {
|
|
212
220
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
213
221
|
return {
|
|
214
|
-
error:
|
|
222
|
+
error: errorMessage,
|
|
215
223
|
success: false,
|
|
216
224
|
};
|
|
217
225
|
}
|
|
@@ -1,8 +1,149 @@
|
|
|
1
|
+
// Escapes a config-supplied operation so it can be embedded in a pattern.
|
|
2
|
+
function escapeForPattern(word) {
|
|
3
|
+
return word.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw `\$&`);
|
|
4
|
+
}
|
|
5
|
+
// String literals ('...', "...") and quoted identifiers (`...`).
|
|
6
|
+
const QUOTE_CHARS = new Set(['"', "'", '`']);
|
|
7
|
+
// Returns the index just past the quoted run that starts at `start`, so the
|
|
8
|
+
// caller can copy a string literal or quoted identifier through untouched. An
|
|
9
|
+
// unterminated quote swallows the rest of the query, which keeps any keyword it
|
|
10
|
+
// hides visible to the safety checks.
|
|
11
|
+
function findQuoteEnd(query, start) {
|
|
12
|
+
const quote = query[start];
|
|
13
|
+
for (let index = start + 1; index < query.length; index += 1) {
|
|
14
|
+
// Backslash escapes apply inside string literals but not inside `identifiers`.
|
|
15
|
+
if (quote !== '`' && query[index] === '\\') {
|
|
16
|
+
index += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (query[index] === quote) {
|
|
20
|
+
// A doubled quote is an escaped quote, not the end of the run.
|
|
21
|
+
if (query[index + 1] === quote) {
|
|
22
|
+
index += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
return index + 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return query.length;
|
|
29
|
+
}
|
|
30
|
+
// Walks the query, yielding each stretch of it alongside the text MySQL would
|
|
31
|
+
// actually execute there. Code and quoted runs come through untouched; an
|
|
32
|
+
// ordinary comment collapses to a single space, so keyword matching sees
|
|
33
|
+
// `DROP /* here */ DATABASE` for what MySQL sees: `DROP DATABASE`.
|
|
34
|
+
//
|
|
35
|
+
// The scan is quote aware — `--`, `#` and `/*` inside a string literal or a
|
|
36
|
+
// quoted identifier are data, not the start of a comment.
|
|
37
|
+
//
|
|
38
|
+
// A `/*! ... */` version comment keeps its body, because MySQL executes it —
|
|
39
|
+
// only the `/*!`, its leading version digits and the closing `*/` become
|
|
40
|
+
// whitespace, so `DROP /*!40000 */ DATABASE` reads as `DROP DATABASE` here too.
|
|
41
|
+
// MySQL 8.4 consumes a five- or six-digit version (`/*!080411 */` runs), and
|
|
42
|
+
// any other digit run it executes as part of the body, where it can only turn
|
|
43
|
+
// the statement into a syntax error — so every leading digit goes, whatever the
|
|
44
|
+
// server's rule for them is.
|
|
45
|
+
// A `/*+ ... */` hint comment goes entirely, like any other comment: its body
|
|
46
|
+
// is hint syntax, never SQL MySQL would run.
|
|
47
|
+
//
|
|
48
|
+
// `quoted` marks a string literal or quoted identifier, so a caller that cares
|
|
49
|
+
// about clause structure rather than raw text can blank it out.
|
|
50
|
+
function* scanQuery(query) {
|
|
51
|
+
let index = 0;
|
|
52
|
+
while (index < query.length) {
|
|
53
|
+
const char = query[index];
|
|
54
|
+
if (QUOTE_CHARS.has(char)) {
|
|
55
|
+
const end = findQuoteEnd(query, index);
|
|
56
|
+
yield { executable: query.slice(index, end), quoted: true, start: index };
|
|
57
|
+
index = end;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (char === '/' && query[index + 1] === '*') {
|
|
61
|
+
const close = query.indexOf('*/', index + 2);
|
|
62
|
+
const end = close === -1 ? query.length : close + 2;
|
|
63
|
+
const body = query.slice(index + 3, close === -1 ? query.length : close);
|
|
64
|
+
const versionComment = query[index + 2] === '!';
|
|
65
|
+
yield { executable: versionComment ? ` ${body.replace(/^\d+/u, ' ')} ` : ' ', quoted: false, start: index };
|
|
66
|
+
index = end;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// `--` only opens a comment when whitespace (or the end of the query)
|
|
70
|
+
// follows it; `a--b` is two minus signs.
|
|
71
|
+
const dashComment = char === '-' && query[index + 1] === '-' && /^\s*$/u.test(query[index + 2] ?? '');
|
|
72
|
+
if (char === '#' || dashComment) {
|
|
73
|
+
const newline = query.indexOf('\n', index);
|
|
74
|
+
yield { executable: ' ', quoted: false, start: index };
|
|
75
|
+
index = newline === -1 ? query.length : newline;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
yield { executable: char, quoted: false, start: index };
|
|
79
|
+
index += 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Replaces every non-executable MySQL comment with a single space, and every
|
|
83
|
+
// string literal or quoted identifier too when `blankQuoted` is set.
|
|
84
|
+
function stripComments(query, blankQuoted = false) {
|
|
85
|
+
let stripped = '';
|
|
86
|
+
for (const { executable, quoted } of scanQuery(query)) {
|
|
87
|
+
stripped += blankQuoted && quoted ? ' ' : executable;
|
|
88
|
+
}
|
|
89
|
+
return stripped;
|
|
90
|
+
}
|
|
91
|
+
// Index of the `;` that terminates the query's single statement — the last one
|
|
92
|
+
// with nothing but whitespace and comments after it — or -1 when the query is
|
|
93
|
+
// not terminated. A `;` inside a string literal or a comment is not a
|
|
94
|
+
// terminator, because MySQL does not execute it as one.
|
|
95
|
+
function findTrailingTerminator(query) {
|
|
96
|
+
let terminator = -1;
|
|
97
|
+
for (const { executable, start } of scanQuery(query)) {
|
|
98
|
+
if (executable === ';') {
|
|
99
|
+
terminator = start;
|
|
100
|
+
}
|
|
101
|
+
else if (/\S/u.test(executable)) {
|
|
102
|
+
// Executable text after a `;` means that `;` separated statements rather
|
|
103
|
+
// than ending the query.
|
|
104
|
+
terminator = -1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return terminator;
|
|
108
|
+
}
|
|
109
|
+
// Strips comments, then trims and upper-cases what is left, giving the checks
|
|
110
|
+
// below a single view of the SQL MySQL would actually execute. Quoted text
|
|
111
|
+
// stays, so a keyword hidden in a string literal is still visible.
|
|
112
|
+
function normalize(query) {
|
|
113
|
+
return stripComments(query).trim().toUpperCase();
|
|
114
|
+
}
|
|
115
|
+
// The same view with every string literal and quoted identifier blanked out.
|
|
116
|
+
// Clause detection asks a structural question — does this statement have a
|
|
117
|
+
// WHERE, does it have a LIMIT — and `SELECT 'LIMIT 5' FROM metrics` has neither,
|
|
118
|
+
// so keeping the quoted text there would drop the row cap off a query that is
|
|
119
|
+
// in fact unbounded. The blacklist and confirmation checks ask the opposite
|
|
120
|
+
// question and keep quoted text on purpose.
|
|
121
|
+
function normalizeClauses(query) {
|
|
122
|
+
return stripComments(query, true).trim().toUpperCase();
|
|
123
|
+
}
|
|
124
|
+
// Tests whether an operation appears in an already-normalized query as whole
|
|
125
|
+
// words, allowing any run of whitespace between the words of a multi-word
|
|
126
|
+
// operation ("DROP DATABASE", "DROP\nDATABASE"). Comments are gone by this
|
|
127
|
+
// point, so a comment between those words no longer hides the operation.
|
|
128
|
+
//
|
|
129
|
+
// The scan deliberately covers the whole query rather than just the leading
|
|
130
|
+
// keyword: a destructive keyword is worth flagging wherever it appears. Word
|
|
131
|
+
// boundaries stop the reverse mistake, where `nowhere_stats` reads as a WHERE
|
|
132
|
+
// clause or `limit_reached` as a LIMIT. A keyword inside a string literal still
|
|
133
|
+
// matches, which errs toward asking for confirmation rather than skipping it —
|
|
134
|
+
// and keeps `PREPARE s FROM 'DROP DATABASE x'` in reach of the blacklist.
|
|
135
|
+
function containsOperation(normalizedQuery, operation) {
|
|
136
|
+
const pattern = operation
|
|
137
|
+
.trim()
|
|
138
|
+
.split(/\s+/)
|
|
139
|
+
.map((word) => escapeForPattern(word))
|
|
140
|
+
.join(String.raw `\s+`);
|
|
141
|
+
return new RegExp(String.raw `\b${pattern}\b`, 'u').test(normalizedQuery);
|
|
142
|
+
}
|
|
1
143
|
export function checkBlacklist(query, blacklistedOperations) {
|
|
2
|
-
const normalizedQuery = query
|
|
144
|
+
const normalizedQuery = normalize(query);
|
|
3
145
|
for (const operation of blacklistedOperations) {
|
|
4
|
-
|
|
5
|
-
if (normalizedQuery.includes(normalizedOp)) {
|
|
146
|
+
if (containsOperation(normalizedQuery, operation.toUpperCase())) {
|
|
6
147
|
return {
|
|
7
148
|
allowed: false,
|
|
8
149
|
reason: `Operation "${operation}" is blacklisted and not allowed`,
|
|
@@ -12,10 +153,9 @@ export function checkBlacklist(query, blacklistedOperations) {
|
|
|
12
153
|
return { allowed: true };
|
|
13
154
|
}
|
|
14
155
|
export function requiresConfirmation(query, confirmationOperations) {
|
|
15
|
-
const normalizedQuery = query
|
|
156
|
+
const normalizedQuery = normalize(query);
|
|
16
157
|
for (const operation of confirmationOperations) {
|
|
17
|
-
|
|
18
|
-
if (normalizedQuery.startsWith(normalizedOp) || normalizedQuery.includes(` ${normalizedOp} `)) {
|
|
158
|
+
if (containsOperation(normalizedQuery, operation.toUpperCase())) {
|
|
19
159
|
return {
|
|
20
160
|
message: `This query contains a destructive operation: ${operation}`,
|
|
21
161
|
required: true,
|
|
@@ -25,7 +165,7 @@ export function requiresConfirmation(query, confirmationOperations) {
|
|
|
25
165
|
return { required: false };
|
|
26
166
|
}
|
|
27
167
|
export function getQueryType(query) {
|
|
28
|
-
const normalizedQuery = query
|
|
168
|
+
const normalizedQuery = normalize(query);
|
|
29
169
|
const firstWord = normalizedQuery.split(/\s+/, 1)[0];
|
|
30
170
|
const knownTypes = [
|
|
31
171
|
'SELECT',
|
|
@@ -47,10 +187,10 @@ export function getQueryType(query) {
|
|
|
47
187
|
}
|
|
48
188
|
export function analyzeQuery(query) {
|
|
49
189
|
const warnings = [];
|
|
50
|
-
const normalizedQuery = query
|
|
190
|
+
const normalizedQuery = normalizeClauses(query);
|
|
51
191
|
// Check for missing WHERE clause in UPDATE/DELETE
|
|
52
192
|
if ((normalizedQuery.startsWith('UPDATE') || normalizedQuery.startsWith('DELETE')) &&
|
|
53
|
-
!normalizedQuery
|
|
193
|
+
!containsOperation(normalizedQuery, 'WHERE')) {
|
|
54
194
|
warnings.push({
|
|
55
195
|
level: 'warning',
|
|
56
196
|
message: 'Missing WHERE clause in UPDATE/DELETE query',
|
|
@@ -66,7 +206,7 @@ export function analyzeQuery(query) {
|
|
|
66
206
|
});
|
|
67
207
|
}
|
|
68
208
|
// Check for missing LIMIT in SELECT
|
|
69
|
-
if (normalizedQuery.startsWith('SELECT') && !normalizedQuery
|
|
209
|
+
if (normalizedQuery.startsWith('SELECT') && !containsOperation(normalizedQuery, 'LIMIT')) {
|
|
70
210
|
warnings.push({
|
|
71
211
|
level: 'info',
|
|
72
212
|
message: 'SELECT query without LIMIT',
|
|
@@ -76,9 +216,17 @@ export function analyzeQuery(query) {
|
|
|
76
216
|
return warnings;
|
|
77
217
|
}
|
|
78
218
|
export function applyDefaultLimit(query, defaultLimit) {
|
|
79
|
-
const normalizedQuery = query
|
|
80
|
-
if (normalizedQuery.startsWith('SELECT')
|
|
81
|
-
return
|
|
219
|
+
const normalizedQuery = normalizeClauses(query);
|
|
220
|
+
if (!normalizedQuery.startsWith('SELECT') || containsOperation(normalizedQuery, 'LIMIT')) {
|
|
221
|
+
return query;
|
|
222
|
+
}
|
|
223
|
+
const terminator = findTrailingTerminator(query);
|
|
224
|
+
// On its own line either way: a trailing `-- comment` would otherwise swallow
|
|
225
|
+
// the LIMIT.
|
|
226
|
+
if (terminator === -1) {
|
|
227
|
+
return `${query.trim()}\nLIMIT ${defaultLimit}`;
|
|
82
228
|
}
|
|
83
|
-
|
|
229
|
+
// The LIMIT has to go in front of the terminator. After it, MySQL reads
|
|
230
|
+
// `LIMIT 100` as a second statement and rejects the whole query.
|
|
231
|
+
return `${query.slice(0, terminator).trim()}\nLIMIT ${defaultLimit}\n${query.slice(terminator).trim()}`;
|
|
84
232
|
}
|
package/oclif.manifest.json
CHANGED
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.6.
|
|
4
|
+
"version": "0.6.2",
|
|
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.12.
|
|
11
|
+
"@hesed/plugin-lib": "^0.12.3",
|
|
12
12
|
"@inquirer/prompts": "^8.3.0",
|
|
13
13
|
"@oclif/core": "^4",
|
|
14
14
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@types/mocha": "^10",
|
|
25
25
|
"@types/node": "^26",
|
|
26
26
|
"@types/sinon": "^22.0.0",
|
|
27
|
-
"c8": "^
|
|
27
|
+
"c8": "^12.0.0",
|
|
28
28
|
"chai": "^6",
|
|
29
29
|
"eslint": "^10",
|
|
30
30
|
"eslint-config-oclif": "^7",
|
|
@@ -69,13 +69,17 @@
|
|
|
69
69
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
70
70
|
"posttest": "npm run lint",
|
|
71
71
|
"prepack": "oclif manifest && oclif readme",
|
|
72
|
-
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
|
72
|
+
"test": "mocha --forbid-only \"test/**/*.test.ts\" --ignore \"test/e2e/**\"",
|
|
73
73
|
"test:coverage": "c8 npm test",
|
|
74
74
|
"test:coverage:report": "c8 --reporter=html --reporter=text npm test",
|
|
75
75
|
"version": "oclif readme && git add README.md",
|
|
76
76
|
"find-deadcode": "ts-prune --ignore '(run|default)'",
|
|
77
77
|
"format": "eslint --cache --fix --quiet . && prettier --write --cache .",
|
|
78
|
-
"pre-commit": "npm run format && npm run find-deadcode"
|
|
78
|
+
"pre-commit": "npm run format && npm run find-deadcode",
|
|
79
|
+
"test:e2e": "./scripts/e2e.sh",
|
|
80
|
+
"e2e:up": "docker compose -f docker/compose.yaml up -d --build --wait",
|
|
81
|
+
"e2e:down": "docker compose -f docker/compose.yaml down -v --remove-orphans",
|
|
82
|
+
"e2e:mocha": "mocha --forbid-only \"test/e2e/**/*.e2e.test.ts\""
|
|
79
83
|
},
|
|
80
84
|
"types": "dist/index.d.ts",
|
|
81
85
|
"c8": {
|