@naturalcycles/mysql-lib 3.11.2 → 3.12.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.
@@ -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 Partial<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, opt.saveMethod === 'insert' ? 'INSERT' : '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
  }
@@ -1,4 +1,4 @@
1
- import { CommonLogger, JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib';
1
+ import { AnyObject, CommonLogger, JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib';
2
2
  export interface MySQLTableStats {
3
3
  Field: string;
4
4
  Type: string;
@@ -14,7 +14,7 @@ export interface MySQLSchemaOptions {
14
14
  * It currently skips nullability and declares everything as "DEFAULT NULL".
15
15
  */
16
16
  export declare function jsonSchemaToMySQLDDL(table: string, schema: JsonSchemaObject<any>, opt?: MySQLSchemaOptions): string;
17
- export declare function mysqlTableStatsToJsonSchemaField<T = any>(table: string, stats: MySQLTableStats[], logger: CommonLogger): JsonSchemaRootObject<T>;
17
+ export declare function mysqlTableStatsToJsonSchemaField<T extends AnyObject = any>(table: string, stats: MySQLTableStats[], logger: CommonLogger): JsonSchemaRootObject<T>;
18
18
  /**
19
19
  * Because MySQL doesn't support `.` in field names and escapes them as tableName + fieldName.
20
20
  *
package/package.json CHANGED
@@ -11,10 +11,10 @@
11
11
  "mysql": "^2.17.1"
12
12
  },
13
13
  "devDependencies": {
14
- "@naturalcycles/dev-lib": "^12.7.5",
15
- "@types/node": "^17.0.5",
14
+ "@naturalcycles/dev-lib": "^13.2.1",
15
+ "@types/node": "^18.7.13",
16
16
  "dotenv": "^16.0.0",
17
- "jest": "^27.2.4"
17
+ "jest": "^29.0.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.2",
39
+ "version": "3.12.2",
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 {
@@ -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
 
@@ -266,25 +283,38 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
266
283
  // SAVE
267
284
  override async saveBatch<ROW extends Partial<ObjectWithId>>(
268
285
  table: string,
269
- _rows: ROW[],
286
+ rowsInput: ROW[],
270
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(
283
- table,
284
- rows,
285
- opt.saveMethod === 'insert' ? 'INSERT' : 'REPLACE',
286
- this.cfg.logger,
287
- )
317
+ const sqls = insertSQL(table, rows, verb, this.cfg.logger)
288
318
 
289
319
  for await (const sql of sqls) {
290
320
  await this.runSQL({ sql })
@@ -302,8 +332,10 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
302
332
  ): Promise<number> {
303
333
  if (!ids.length) return 0
304
334
  const sql = dbQueryToSQLDelete(new DBQuery(table).filterEq('id', ids))
305
- const res = await this.runSQL<any>({ sql })
306
- return res.affectedRows
335
+ if (!sql) return 0
336
+
337
+ const { affectedRows } = await this.runSQL<OkPacket>({ sql })
338
+ return affectedRows
307
339
  }
308
340
 
309
341
  override async deleteByQuery<ROW extends ObjectWithId>(
@@ -311,8 +343,9 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
311
343
  _opt?: CommonDBOptions,
312
344
  ): Promise<number> {
313
345
  const sql = dbQueryToSQLDelete(q)
314
- const res = await this.runSQL<any>({ sql })
315
- return res.affectedRows
346
+ if (!sql) return 0
347
+ const { affectedRows } = await this.runSQL<OkPacket>({ sql })
348
+ return affectedRows
316
349
  }
317
350
 
318
351
  /**
package/src/query.util.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { DBQuery } from '@naturalcycles/db-lib'
2
- import { DBQueryFilterOperator } from '@naturalcycles/db-lib/src/query/dbQuery'
1
+ import { DBQuery, DBQueryFilterOperator } from '@naturalcycles/db-lib'
3
2
  import { _hb, CommonLogger } from '@naturalcycles/js-lib'
4
3
  import { white, yellow } from '@naturalcycles/nodejs-lib/dist/colors'
5
4
  import { QueryOptions } from 'mysql'
@@ -9,11 +8,17 @@ import { mapNameToMySQL } from './schema/mysql.schema.util'
9
8
  const MAX_PACKET_SIZE = 1024 * 1024 // 1Mb
10
9
  const MAX_ROW_SIZE = 800 * 1024 // 1Mb - margin
11
10
 
12
- export function dbQueryToSQLSelect(q: DBQuery<any>): string {
11
+ /**
12
+ * Returns `null` if it detects that 0 rows will be returned,
13
+ * e.g when `IN ()` (empty array) is used.
14
+ */
15
+ export function dbQueryToSQLSelect(q: DBQuery<any>): string | null {
13
16
  const tokens = selectTokens(q)
14
17
 
15
18
  // filters
16
- tokens.push(...whereTokens(q))
19
+ const whereTokens = getWhereTokens(q)
20
+ if (!whereTokens) return null
21
+ tokens.push(...whereTokens)
17
22
 
18
23
  // order
19
24
  tokens.push(...groupOrderTokens(q))
@@ -24,11 +29,16 @@ export function dbQueryToSQLSelect(q: DBQuery<any>): string {
24
29
  return tokens.join(' ')
25
30
  }
26
31
 
27
- export function dbQueryToSQLDelete(q: DBQuery<any>): string {
32
+ /**
33
+ * Returns null in "0 rows" case.
34
+ */
35
+ export function dbQueryToSQLDelete(q: DBQuery<any>): string | null {
28
36
  const tokens = [`DELETE FROM`, mysql.escapeId(q.table)]
29
37
 
30
38
  // filters
31
- tokens.push(...whereTokens(q))
39
+ const whereTokens = getWhereTokens(q)
40
+ if (!whereTokens) return null
41
+ tokens.push(...whereTokens)
32
42
 
33
43
  // offset/limit
34
44
  tokens.push(...offsetLimitTokens(q))
@@ -132,8 +142,11 @@ export function insertSQLSetSingle(table: string, record: Record<any, any>): Que
132
142
  }
133
143
  }
134
144
 
135
- export function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string {
145
+ export function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string | null {
136
146
  // var sql = mysql.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]);
147
+ const whereTokens = getWhereTokens(q)
148
+ if (!whereTokens) return null
149
+
137
150
  const tokens = [
138
151
  `UPDATE`,
139
152
  mysql.escapeId(q.table),
@@ -141,7 +154,7 @@ export function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): s
141
154
  Object.keys(record)
142
155
  .map(f => mysql.escapeId(mapNameToMySQL(f)) + ' = ?')
143
156
  .join(', '),
144
- ...whereTokens(q),
157
+ ...whereTokens,
145
158
  ]
146
159
 
147
160
  return mysql.format(tokens.join(' '), Object.values(record))
@@ -200,10 +213,15 @@ const OP_MAP: Partial<Record<DBQueryFilterOperator, string>> = {
200
213
  '==': '=',
201
214
  }
202
215
 
203
- function whereTokens(q: DBQuery): string[] {
216
+ /**
217
+ * Returns `null` for "guaranteed 0 rows" cases.
218
+ */
219
+ function getWhereTokens(q: DBQuery): string[] | null {
204
220
  if (!q._filters.length) return []
205
221
 
206
- return [
222
+ let returnNull = false
223
+
224
+ const tokens = [
207
225
  `WHERE`,
208
226
  q._filters
209
227
  .map(f => {
@@ -218,6 +236,8 @@ function whereTokens(q: DBQuery): string[] {
218
236
 
219
237
  if (Array.isArray(f.val)) {
220
238
  // special case for arrays
239
+ if (!f.val.length) returnNull = true
240
+
221
241
  return `${mysql.escapeId(mapNameToMySQL(f.name as string))} IN (${mysql.escape(f.val)})`
222
242
  }
223
243
 
@@ -229,4 +249,8 @@ function whereTokens(q: DBQuery): string[] {
229
249
  })
230
250
  .join(' AND '),
231
251
  ]
252
+
253
+ if (returnNull) return null
254
+
255
+ return tokens
232
256
  }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AnyObject,
2
3
  CommonLogger,
3
4
  JsonSchemaBoolean,
4
5
  JsonSchemaNumber,
@@ -79,7 +80,7 @@ export function jsonSchemaToMySQLDDL(
79
80
  return lines.join('\n')
80
81
  }
81
82
 
82
- export function mysqlTableStatsToJsonSchemaField<T = any>(
83
+ export function mysqlTableStatsToJsonSchemaField<T extends AnyObject = any>(
83
84
  table: string,
84
85
  stats: MySQLTableStats[],
85
86
  logger: CommonLogger,
@@ -93,7 +94,7 @@ export function mysqlTableStatsToJsonSchemaField<T = any>(
93
94
  }
94
95
 
95
96
  stats.forEach(stat => {
96
- const name = stat.Field
97
+ const name = stat.Field as keyof T
97
98
  const t = stat.Type.toLowerCase()
98
99
  const notNull = stat.Null?.toUpperCase() !== 'YES'
99
100
  if (notNull) {
@@ -112,7 +113,7 @@ export function mysqlTableStatsToJsonSchemaField<T = any>(
112
113
  s.properties[name] = { type: 'number' } as JsonSchemaNumber
113
114
  } else {
114
115
  logger.log(s)
115
- throw new Error(`Unknown mysql field type ${name} ${stat.Type}`)
116
+ throw new Error(`Unknown mysql field type ${name as string} ${stat.Type}`)
116
117
  }
117
118
  })
118
119