@naturalcycles/mysql-lib 3.11.1 → 3.12.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.
@@ -4,7 +4,7 @@ import { ReadableTyped } from '@naturalcycles/nodejs-lib';
4
4
  import { Connection, Pool, PoolConfig, PoolConnection, QueryOptions } from 'mysql';
5
5
  export interface MysqlDBOptions extends CommonDBOptions {
6
6
  }
7
- export interface MysqlDBSaveOptions<ROW extends ObjectWithId = AnyObjectWithId> extends CommonDBSaveOptions<ROW> {
7
+ export interface MysqlDBSaveOptions<ROW extends Partial<ObjectWithId> = AnyObjectWithId> extends CommonDBSaveOptions<ROW> {
8
8
  }
9
9
  /**
10
10
  * @default false / undefined
@@ -50,7 +50,7 @@ export declare class MysqlDB extends BaseCommonDB implements CommonDB {
50
50
  runSQLString(s: string): Promise<void>;
51
51
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
52
52
  streamQuery<ROW extends ObjectWithId, OUT = ROW>(q: DBQuery<ROW>, _opt?: MysqlDBOptions): ReadableTyped<OUT>;
53
- saveBatch<ROW extends ObjectWithId>(table: string, _rows: ROW[], _opt?: MysqlDBSaveOptions<ROW>): Promise<void>;
53
+ saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rowsInput: ROW[], opt?: MysqlDBSaveOptions<ROW>): Promise<void>;
54
54
  /**
55
55
  * Limitation: always returns [], regardless of which rows are actually deleted
56
56
  */
package/dist/mysql.db.js CHANGED
@@ -116,6 +116,11 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
116
116
  // QUERY
117
117
  async runQuery(q, _opt = {}) {
118
118
  const sql = (0, query_util_1.dbQueryToSQLSelect)(q);
119
+ if (!sql) {
120
+ return {
121
+ rows: [],
122
+ };
123
+ }
119
124
  const rows = (await this.runSQL({ sql })).map(row => (0, js_lib_1._mapKeys)((0, js_lib_1._filterUndefinedValues)(row, true), k => (0, mysql_schema_util_1.mapNameFromMySQL)(k)));
120
125
  // edge case where 0 fields are selected
121
126
  if (q._selectedFieldNames?.length === 0) {
@@ -157,6 +162,9 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
157
162
  }
158
163
  streamQuery(q, _opt = {}) {
159
164
  const sql = (0, query_util_1.dbQueryToSQLSelect)(q);
165
+ if (!sql) {
166
+ return stream_1.Readable.from([]);
167
+ }
160
168
  if (this.cfg.logSQL)
161
169
  this.cfg.logger.log(`stream: ${sql}`);
162
170
  // return this.streamSQL(sql, opt)
@@ -171,15 +179,28 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
171
179
  }));
172
180
  }
173
181
  // SAVE
174
- async saveBatch(table, _rows, _opt) {
175
- if (!_rows.length)
182
+ async saveBatch(table, rowsInput, opt = {}) {
183
+ if (!rowsInput.length)
176
184
  return;
177
185
  // Stringify object values
178
- const rows = _rows.map(row => (0, js_lib_1._mapValues)(row, (_k, v) => {
186
+ const rows = rowsInput.map(row => (0, js_lib_1._mapValues)(row, (_k, v) => {
179
187
  return v && typeof v === 'object' && !Buffer.isBuffer(v) ? JSON.stringify(v) : v;
180
188
  }));
189
+ const verb = opt.saveMethod === 'insert' ? 'INSERT' : 'REPLACE';
190
+ if (opt.assignGeneratedIds) {
191
+ // Insert rows one-by-one, to get their auto-generated id
192
+ let i = -1;
193
+ for await (const row of rows) {
194
+ i++;
195
+ const sql = (0, query_util_1.insertSQL)(table, [row], verb, this.cfg.logger)[0];
196
+ const { insertId } = await this.runSQL({ sql });
197
+ // Mutate the input row with insertIt
198
+ rowsInput[i].id = insertId;
199
+ }
200
+ return;
201
+ }
181
202
  // inserts are split into multiple sentenses to respect the max_packet_size (1Mb usually)
182
- const sqls = (0, query_util_1.insertSQL)(table, rows, 'REPLACE', this.cfg.logger);
203
+ const sqls = (0, query_util_1.insertSQL)(table, rows, verb, this.cfg.logger);
183
204
  for await (const sql of sqls) {
184
205
  await this.runSQL({ sql });
185
206
  }
@@ -192,13 +213,17 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
192
213
  if (!ids.length)
193
214
  return 0;
194
215
  const sql = (0, query_util_1.dbQueryToSQLDelete)(new db_lib_1.DBQuery(table).filterEq('id', ids));
195
- const res = await this.runSQL({ sql });
196
- return res.affectedRows;
216
+ if (!sql)
217
+ return 0;
218
+ const { affectedRows } = await this.runSQL({ sql });
219
+ return affectedRows;
197
220
  }
198
221
  async deleteByQuery(q, _opt) {
199
222
  const sql = (0, query_util_1.dbQueryToSQLDelete)(q);
200
- const res = await this.runSQL({ sql });
201
- return res.affectedRows;
223
+ if (!sql)
224
+ return 0;
225
+ const { affectedRows } = await this.runSQL({ sql });
226
+ return affectedRows;
202
227
  }
203
228
  /**
204
229
  * Use with caution!
@@ -1,12 +1,19 @@
1
1
  import { DBQuery } from '@naturalcycles/db-lib';
2
2
  import { CommonLogger } from '@naturalcycles/js-lib';
3
3
  import { QueryOptions } from 'mysql';
4
- export declare function dbQueryToSQLSelect(q: DBQuery<any>): string;
5
- export declare function dbQueryToSQLDelete(q: DBQuery<any>): string;
4
+ /**
5
+ * Returns `null` if it detects that 0 rows will be returned,
6
+ * e.g when `IN ()` (empty array) is used.
7
+ */
8
+ export declare function dbQueryToSQLSelect(q: DBQuery<any>): string | null;
9
+ /**
10
+ * Returns null in "0 rows" case.
11
+ */
12
+ export declare function dbQueryToSQLDelete(q: DBQuery<any>): string | null;
6
13
  /**
7
14
  * Returns array of sql statements to respect the max sql size.
8
15
  */
9
16
  export declare function insertSQL(table: string, rows: Record<any, any>[], verb?: 'INSERT' | 'REPLACE', logger?: CommonLogger): string[];
10
17
  export declare function insertSQLSingle(table: string, record: Record<any, any>): string;
11
18
  export declare function insertSQLSetSingle(table: string, record: Record<any, any>): QueryOptions;
12
- export declare function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string;
19
+ export declare function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string | null;
@@ -7,10 +7,17 @@ const mysql = require("mysql");
7
7
  const mysql_schema_util_1 = require("./schema/mysql.schema.util");
8
8
  const MAX_PACKET_SIZE = 1024 * 1024; // 1Mb
9
9
  const MAX_ROW_SIZE = 800 * 1024; // 1Mb - margin
10
+ /**
11
+ * Returns `null` if it detects that 0 rows will be returned,
12
+ * e.g when `IN ()` (empty array) is used.
13
+ */
10
14
  function dbQueryToSQLSelect(q) {
11
15
  const tokens = selectTokens(q);
12
16
  // filters
13
- tokens.push(...whereTokens(q));
17
+ const whereTokens = getWhereTokens(q);
18
+ if (!whereTokens)
19
+ return null;
20
+ tokens.push(...whereTokens);
14
21
  // order
15
22
  tokens.push(...groupOrderTokens(q));
16
23
  // offset/limit
@@ -18,10 +25,16 @@ function dbQueryToSQLSelect(q) {
18
25
  return tokens.join(' ');
19
26
  }
20
27
  exports.dbQueryToSQLSelect = dbQueryToSQLSelect;
28
+ /**
29
+ * Returns null in "0 rows" case.
30
+ */
21
31
  function dbQueryToSQLDelete(q) {
22
32
  const tokens = [`DELETE FROM`, mysql.escapeId(q.table)];
23
33
  // filters
24
- tokens.push(...whereTokens(q));
34
+ const whereTokens = getWhereTokens(q);
35
+ if (!whereTokens)
36
+ return null;
37
+ tokens.push(...whereTokens);
25
38
  // offset/limit
26
39
  tokens.push(...offsetLimitTokens(q));
27
40
  return tokens.join(' ');
@@ -106,6 +119,9 @@ function insertSQLSetSingle(table, record) {
106
119
  exports.insertSQLSetSingle = insertSQLSetSingle;
107
120
  function dbQueryToSQLUpdate(q, record) {
108
121
  // var sql = mysql.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]);
122
+ const whereTokens = getWhereTokens(q);
123
+ if (!whereTokens)
124
+ return null;
109
125
  const tokens = [
110
126
  `UPDATE`,
111
127
  mysql.escapeId(q.table),
@@ -113,7 +129,7 @@ function dbQueryToSQLUpdate(q, record) {
113
129
  Object.keys(record)
114
130
  .map(f => mysql.escapeId((0, mysql_schema_util_1.mapNameToMySQL)(f)) + ' = ?')
115
131
  .join(', '),
116
- ...whereTokens(q),
132
+ ...whereTokens,
117
133
  ];
118
134
  return mysql.format(tokens.join(' '), Object.values(record));
119
135
  }
@@ -156,10 +172,14 @@ function groupOrderTokens(q) {
156
172
  const OP_MAP = {
157
173
  '==': '=',
158
174
  };
159
- function whereTokens(q) {
175
+ /**
176
+ * Returns `null` for "guaranteed 0 rows" cases.
177
+ */
178
+ function getWhereTokens(q) {
160
179
  if (!q._filters.length)
161
180
  return [];
162
- return [
181
+ let returnNull = false;
182
+ const tokens = [
163
183
  `WHERE`,
164
184
  q._filters
165
185
  .map(f => {
@@ -172,6 +192,8 @@ function whereTokens(q) {
172
192
  }
173
193
  if (Array.isArray(f.val)) {
174
194
  // special case for arrays
195
+ if (!f.val.length)
196
+ returnNull = true;
175
197
  return `${mysql.escapeId((0, mysql_schema_util_1.mapNameToMySQL)(f.name))} IN (${mysql.escape(f.val)})`;
176
198
  }
177
199
  return [
@@ -182,4 +204,7 @@ function whereTokens(q) {
182
204
  })
183
205
  .join(' AND '),
184
206
  ];
207
+ if (returnNull)
208
+ return null;
209
+ return tokens;
185
210
  }
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "@naturalcycles/dev-lib": "^12.7.5",
15
15
  "@types/node": "^17.0.5",
16
16
  "dotenv": "^16.0.0",
17
- "jest": "^27.2.4"
17
+ "jest": "^28.1.0"
18
18
  },
19
19
  "files": [
20
20
  "dist",
@@ -36,7 +36,7 @@
36
36
  "engines": {
37
37
  "node": ">=14.15.0"
38
38
  },
39
- "version": "3.11.1",
39
+ "version": "3.12.1",
40
40
  "description": "MySQL client implementing CommonDB interface",
41
41
  "keywords": [
42
42
  "mysql",
package/src/mysql.db.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Transform } from 'stream'
1
+ import { Readable, Transform } from 'stream'
2
2
  import { promisify } from 'util'
3
3
  import {
4
4
  BaseCommonDB,
@@ -23,7 +23,15 @@ import {
23
23
  } from '@naturalcycles/js-lib'
24
24
  import { ReadableTyped } from '@naturalcycles/nodejs-lib'
25
25
  import { white } from '@naturalcycles/nodejs-lib/dist/colors'
26
- import { Connection, Pool, PoolConfig, PoolConnection, QueryOptions, TypeCast } from 'mysql'
26
+ import {
27
+ Connection,
28
+ OkPacket,
29
+ Pool,
30
+ PoolConfig,
31
+ PoolConnection,
32
+ QueryOptions,
33
+ TypeCast,
34
+ } from 'mysql'
27
35
  import * as mysql from 'mysql'
28
36
  import { dbQueryToSQLDelete, dbQueryToSQLSelect, insertSQL } from './query.util'
29
37
  import {
@@ -34,7 +42,7 @@ import {
34
42
  } from './schema/mysql.schema.util'
35
43
 
36
44
  export interface MysqlDBOptions extends CommonDBOptions {}
37
- export interface MysqlDBSaveOptions<ROW extends ObjectWithId = AnyObjectWithId>
45
+ export interface MysqlDBSaveOptions<ROW extends Partial<ObjectWithId> = AnyObjectWithId>
38
46
  extends CommonDBSaveOptions<ROW> {}
39
47
 
40
48
  /**
@@ -191,6 +199,12 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
191
199
  _opt: MysqlDBOptions = {},
192
200
  ): Promise<RunQueryResult<OUT>> {
193
201
  const sql = dbQueryToSQLSelect(q)
202
+ if (!sql) {
203
+ return {
204
+ rows: [],
205
+ }
206
+ }
207
+
194
208
  const rows = (await this.runSQL<ROW[]>({ sql })).map(
195
209
  row => _mapKeys(_filterUndefinedValues(row, true), k => mapNameFromMySQL(k)) as any,
196
210
  )
@@ -246,6 +260,9 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
246
260
  _opt: MysqlDBOptions = {},
247
261
  ): ReadableTyped<OUT> {
248
262
  const sql = dbQueryToSQLSelect(q)
263
+ if (!sql) {
264
+ return Readable.from([])
265
+ }
249
266
 
250
267
  if (this.cfg.logSQL) this.cfg.logger.log(`stream: ${sql}`)
251
268
 
@@ -264,22 +281,40 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
264
281
  }
265
282
 
266
283
  // SAVE
267
- override async saveBatch<ROW extends ObjectWithId>(
284
+ override async saveBatch<ROW extends Partial<ObjectWithId>>(
268
285
  table: string,
269
- _rows: ROW[],
270
- _opt?: MysqlDBSaveOptions<ROW>,
286
+ rowsInput: ROW[],
287
+ opt: MysqlDBSaveOptions<ROW> = {},
271
288
  ): Promise<void> {
272
- if (!_rows.length) return
289
+ if (!rowsInput.length) return
273
290
 
274
291
  // Stringify object values
275
- const rows = _rows.map(row =>
292
+ const rows = rowsInput.map(row =>
276
293
  _mapValues(row, (_k, v) => {
277
294
  return v && typeof v === 'object' && !Buffer.isBuffer(v) ? JSON.stringify(v) : v
278
295
  }),
279
296
  )
280
297
 
298
+ const verb = opt.saveMethod === 'insert' ? 'INSERT' : 'REPLACE'
299
+
300
+ if (opt.assignGeneratedIds) {
301
+ // Insert rows one-by-one, to get their auto-generated id
302
+
303
+ let i = -1
304
+ for await (const row of rows) {
305
+ i++
306
+ const sql = insertSQL(table, [row], verb, this.cfg.logger)[0]!
307
+ const { insertId } = await this.runSQL<OkPacket>({ sql })
308
+
309
+ // Mutate the input row with insertIt
310
+ rowsInput[i]!.id = insertId
311
+ }
312
+
313
+ return
314
+ }
315
+
281
316
  // inserts are split into multiple sentenses to respect the max_packet_size (1Mb usually)
282
- const sqls = insertSQL(table, rows, 'REPLACE', this.cfg.logger)
317
+ const sqls = insertSQL(table, rows, verb, this.cfg.logger)
283
318
 
284
319
  for await (const sql of sqls) {
285
320
  await this.runSQL({ sql })
@@ -297,8 +332,10 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
297
332
  ): Promise<number> {
298
333
  if (!ids.length) return 0
299
334
  const sql = dbQueryToSQLDelete(new DBQuery(table).filterEq('id', ids))
300
- const res = await this.runSQL<any>({ sql })
301
- return res.affectedRows
335
+ if (!sql) return 0
336
+
337
+ const { affectedRows } = await this.runSQL<OkPacket>({ sql })
338
+ return affectedRows
302
339
  }
303
340
 
304
341
  override async deleteByQuery<ROW extends ObjectWithId>(
@@ -306,8 +343,9 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
306
343
  _opt?: CommonDBOptions,
307
344
  ): Promise<number> {
308
345
  const sql = dbQueryToSQLDelete(q)
309
- const res = await this.runSQL<any>({ sql })
310
- return res.affectedRows
346
+ if (!sql) return 0
347
+ const { affectedRows } = await this.runSQL<OkPacket>({ sql })
348
+ return affectedRows
311
349
  }
312
350
 
313
351
  /**
package/src/query.util.ts CHANGED
@@ -9,11 +9,17 @@ import { mapNameToMySQL } from './schema/mysql.schema.util'
9
9
  const MAX_PACKET_SIZE = 1024 * 1024 // 1Mb
10
10
  const MAX_ROW_SIZE = 800 * 1024 // 1Mb - margin
11
11
 
12
- export function dbQueryToSQLSelect(q: DBQuery<any>): string {
12
+ /**
13
+ * Returns `null` if it detects that 0 rows will be returned,
14
+ * e.g when `IN ()` (empty array) is used.
15
+ */
16
+ export function dbQueryToSQLSelect(q: DBQuery<any>): string | null {
13
17
  const tokens = selectTokens(q)
14
18
 
15
19
  // filters
16
- tokens.push(...whereTokens(q))
20
+ const whereTokens = getWhereTokens(q)
21
+ if (!whereTokens) return null
22
+ tokens.push(...whereTokens)
17
23
 
18
24
  // order
19
25
  tokens.push(...groupOrderTokens(q))
@@ -24,11 +30,16 @@ export function dbQueryToSQLSelect(q: DBQuery<any>): string {
24
30
  return tokens.join(' ')
25
31
  }
26
32
 
27
- export function dbQueryToSQLDelete(q: DBQuery<any>): string {
33
+ /**
34
+ * Returns null in "0 rows" case.
35
+ */
36
+ export function dbQueryToSQLDelete(q: DBQuery<any>): string | null {
28
37
  const tokens = [`DELETE FROM`, mysql.escapeId(q.table)]
29
38
 
30
39
  // filters
31
- tokens.push(...whereTokens(q))
40
+ const whereTokens = getWhereTokens(q)
41
+ if (!whereTokens) return null
42
+ tokens.push(...whereTokens)
32
43
 
33
44
  // offset/limit
34
45
  tokens.push(...offsetLimitTokens(q))
@@ -132,8 +143,11 @@ export function insertSQLSetSingle(table: string, record: Record<any, any>): Que
132
143
  }
133
144
  }
134
145
 
135
- export function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string {
146
+ export function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string | null {
136
147
  // var sql = mysql.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]);
148
+ const whereTokens = getWhereTokens(q)
149
+ if (!whereTokens) return null
150
+
137
151
  const tokens = [
138
152
  `UPDATE`,
139
153
  mysql.escapeId(q.table),
@@ -141,7 +155,7 @@ export function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): s
141
155
  Object.keys(record)
142
156
  .map(f => mysql.escapeId(mapNameToMySQL(f)) + ' = ?')
143
157
  .join(', '),
144
- ...whereTokens(q),
158
+ ...whereTokens,
145
159
  ]
146
160
 
147
161
  return mysql.format(tokens.join(' '), Object.values(record))
@@ -200,10 +214,15 @@ const OP_MAP: Partial<Record<DBQueryFilterOperator, string>> = {
200
214
  '==': '=',
201
215
  }
202
216
 
203
- function whereTokens(q: DBQuery): string[] {
217
+ /**
218
+ * Returns `null` for "guaranteed 0 rows" cases.
219
+ */
220
+ function getWhereTokens(q: DBQuery): string[] | null {
204
221
  if (!q._filters.length) return []
205
222
 
206
- return [
223
+ let returnNull = false
224
+
225
+ const tokens = [
207
226
  `WHERE`,
208
227
  q._filters
209
228
  .map(f => {
@@ -218,6 +237,8 @@ function whereTokens(q: DBQuery): string[] {
218
237
 
219
238
  if (Array.isArray(f.val)) {
220
239
  // special case for arrays
240
+ if (!f.val.length) returnNull = true
241
+
221
242
  return `${mysql.escapeId(mapNameToMySQL(f.name as string))} IN (${mysql.escape(f.val)})`
222
243
  }
223
244
 
@@ -229,4 +250,8 @@ function whereTokens(q: DBQuery): string[] {
229
250
  })
230
251
  .join(' AND '),
231
252
  ]
253
+
254
+ if (returnNull) return null
255
+
256
+ return tokens
232
257
  }