@naturalcycles/mysql-lib 3.9.0 → 3.11.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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getDBAdapter } from './dbAdapter';
2
2
  import { MysqlDB, MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions } from './mysql.db';
3
+ import { MySQLKeyValueDB } from './mysqlKeyValueDB';
3
4
  import { jsonSchemaToMySQLDDL } from './schema/mysql.schema.util';
4
5
  export type { MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions };
5
- export { MysqlDB, jsonSchemaToMySQLDDL, getDBAdapter };
6
+ export { MysqlDB, MySQLKeyValueDB, jsonSchemaToMySQLDDL, getDBAdapter };
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getDBAdapter = exports.jsonSchemaToMySQLDDL = exports.MysqlDB = void 0;
3
+ exports.getDBAdapter = exports.jsonSchemaToMySQLDDL = exports.MySQLKeyValueDB = exports.MysqlDB = void 0;
4
4
  const dbAdapter_1 = require("./dbAdapter");
5
5
  Object.defineProperty(exports, "getDBAdapter", { enumerable: true, get: function () { return dbAdapter_1.getDBAdapter; } });
6
6
  const mysql_db_1 = require("./mysql.db");
7
7
  Object.defineProperty(exports, "MysqlDB", { enumerable: true, get: function () { return mysql_db_1.MysqlDB; } });
8
+ const mysqlKeyValueDB_1 = require("./mysqlKeyValueDB");
9
+ Object.defineProperty(exports, "MySQLKeyValueDB", { enumerable: true, get: function () { return mysqlKeyValueDB_1.MySQLKeyValueDB; } });
8
10
  const mysql_schema_util_1 = require("./schema/mysql.schema.util");
9
11
  Object.defineProperty(exports, "jsonSchemaToMySQLDDL", { enumerable: true, get: function () { return mysql_schema_util_1.jsonSchemaToMySQLDDL; } });
@@ -43,6 +43,11 @@ export declare class MysqlDB extends BaseCommonDB implements CommonDB {
43
43
  getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: MysqlDBOptions): Promise<ROW[]>;
44
44
  runQuery<ROW extends ObjectWithId, OUT = ROW>(q: DBQuery<ROW>, _opt?: MysqlDBOptions): Promise<RunQueryResult<OUT>>;
45
45
  runSQL<RESULT>(q: QueryOptions): Promise<RESULT>;
46
+ /**
47
+ * Allows to run semicolon-separated "SQL file".
48
+ * E.g "ddl reset script".
49
+ */
50
+ runSQLString(s: string): Promise<void>;
46
51
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
47
52
  streamQuery<ROW extends ObjectWithId, OUT = ROW>(q: DBQuery<ROW>, _opt?: MysqlDBOptions): ReadableTyped<OUT>;
48
53
  saveBatch<ROW extends ObjectWithId>(table: string, _rows: ROW[], _opt?: MysqlDBSaveOptions<ROW>): Promise<void>;
package/dist/mysql.db.js CHANGED
@@ -75,7 +75,7 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
75
75
  async close() {
76
76
  const pool = this.pool();
77
77
  // eslint-disable-next-line @typescript-eslint/await-thenable
78
- await (0, util_1.promisify)(pool.end.bind(pool));
78
+ await (0, util_1.promisify)(pool.end.bind(pool))();
79
79
  this.cfg.logger.log('closed');
80
80
  }
81
81
  /**
@@ -116,14 +116,20 @@ 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
- const rows = await this.runSQL({ sql });
119
+ 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
+ // edge case where 0 fields are selected
121
+ if (q._selectedFieldNames?.length === 0) {
122
+ return {
123
+ rows: rows.map(_ => ({})),
124
+ };
125
+ }
120
126
  return {
121
- rows: rows.map(r => (0, js_lib_1._mapKeys)((0, js_lib_1._filterNullishValues)(r, true), k => (0, mysql_schema_util_1.mapNameFromMySQL)(k))),
127
+ rows,
122
128
  };
123
129
  }
124
130
  async runSQL(q) {
125
131
  if (this.cfg.logSQL)
126
- this.cfg.logger.log(q.sql, q.values);
132
+ this.cfg.logger.log(...[q.sql, q.values].filter(Boolean));
127
133
  return await new Promise((resolve, reject) => {
128
134
  this.pool().query(q, (err, res) => {
129
135
  if (err)
@@ -132,6 +138,19 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
132
138
  });
133
139
  });
134
140
  }
141
+ /**
142
+ * Allows to run semicolon-separated "SQL file".
143
+ * E.g "ddl reset script".
144
+ */
145
+ async runSQLString(s) {
146
+ const queries = s
147
+ .split(';')
148
+ .map(s => s.trim())
149
+ .filter(Boolean);
150
+ for await (const sql of queries) {
151
+ await this.runSQL({ sql });
152
+ }
153
+ }
135
154
  async runQueryCount(q, _opt) {
136
155
  const { rows } = await this.runQuery(q.select(['count(*) as _count']));
137
156
  return rows[0]._count;
@@ -147,7 +166,7 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
147
166
  .pipe(new stream_1.Transform({
148
167
  objectMode: true,
149
168
  transform(row, _encoding, cb) {
150
- cb(null, (0, js_lib_1._filterNullishValues)(row, true));
169
+ cb(null, (0, js_lib_1._filterUndefinedValues)(row, true));
151
170
  },
152
171
  }));
153
172
  }
@@ -156,11 +175,11 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
156
175
  if (!_rows.length)
157
176
  return;
158
177
  // Stringify object values
159
- const rows = _rows.map(row => (0, js_lib_1._mapValues)(row, v => {
178
+ const rows = _rows.map(row => (0, js_lib_1._mapValues)(row, (_k, v) => {
160
179
  return v && typeof v === 'object' && !Buffer.isBuffer(v) ? JSON.stringify(v) : v;
161
180
  }));
162
181
  // inserts are split into multiple sentenses to respect the max_packet_size (1Mb usually)
163
- const sqls = (0, query_util_1.insertSQL)(table, rows, 'INSERT', this.cfg.logger);
182
+ const sqls = (0, query_util_1.insertSQL)(table, rows, 'REPLACE', this.cfg.logger);
164
183
  for await (const sql of sqls) {
165
184
  await this.runSQL({ sql });
166
185
  }
@@ -0,0 +1,25 @@
1
+ /// <reference types="node" />
2
+ import { CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib';
3
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib';
4
+ import { MysqlDB, MysqlDBCfg } from './mysql.db';
5
+ export declare class MySQLKeyValueDB implements CommonKeyValueDB {
6
+ cfg: MysqlDBCfg;
7
+ constructor(cfg: MysqlDBCfg);
8
+ db: MysqlDB;
9
+ ping(): Promise<void>;
10
+ close(): Promise<void>;
11
+ createTable(table: string, opt?: CommonDBCreateOptions): Promise<void>;
12
+ getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]>;
13
+ /**
14
+ * Use with caution!
15
+ */
16
+ dropTable(table: string): Promise<void>;
17
+ deleteByIds(table: string, ids: string[]): Promise<void>;
18
+ saveBatch(table: string, entries: KeyValueDBTuple[]): Promise<void>;
19
+ streamIds(table: string, limit?: number): ReadableTyped<string>;
20
+ streamValues(table: string, limit?: number): ReadableTyped<Buffer>;
21
+ streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple>;
22
+ beginTransaction(): Promise<void>;
23
+ endTransaction(): Promise<void>;
24
+ count(table: string): Promise<number>;
25
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MySQLKeyValueDB = void 0;
4
+ const stream_1 = require("stream");
5
+ const js_lib_1 = require("@naturalcycles/js-lib");
6
+ const mysql_db_1 = require("./mysql.db");
7
+ class MySQLKeyValueDB {
8
+ constructor(cfg) {
9
+ this.cfg = cfg;
10
+ this.db = new mysql_db_1.MysqlDB(this.cfg);
11
+ }
12
+ async ping() {
13
+ await this.db.ping();
14
+ }
15
+ async close() {
16
+ await this.db.close();
17
+ }
18
+ async createTable(table, opt = {}) {
19
+ if (opt.dropIfExists)
20
+ await this.dropTable(table);
21
+ // On blob sizes: https://tableplus.com/blog/2019/10/tinyblob-blob-mediumblob-longblob.html
22
+ // LONGBLOB supports up to 4gb
23
+ // MEDIUMBLOB up to 16Mb
24
+ const sql = `create table ${table} (id VARCHAR(64) PRIMARY KEY, v LONGBLOB NOT NULL)`;
25
+ this.db.cfg.logger.log(sql);
26
+ await this.db.runSQL({ sql });
27
+ }
28
+ async getByIds(table, ids) {
29
+ if (!ids.length)
30
+ return [];
31
+ const sql = `SELECT id,v FROM ${table} where id in (${ids.map(id => `"${id}"`).join(',')})`;
32
+ const rows = await this.db.runSQL({ sql });
33
+ return rows.map(({ id, v }) => [id, v]);
34
+ }
35
+ /**
36
+ * Use with caution!
37
+ */
38
+ async dropTable(table) {
39
+ await this.db.runSQL({ sql: `DROP TABLE IF EXISTS ${table}` });
40
+ }
41
+ async deleteByIds(table, ids) {
42
+ const sql = `DELETE FROM ${table} WHERE id in (${ids.map(id => `"${id}"`).join(',')})`;
43
+ if (this.cfg.logSQL)
44
+ this.db.cfg.logger.log(sql);
45
+ await this.db.runSQL({ sql });
46
+ }
47
+ async saveBatch(table, entries) {
48
+ const statements = entries.map(([id, buf]) => {
49
+ return {
50
+ sql: `INSERT INTO ${table} (id, v) VALUES (?, ?)`,
51
+ values: [id, buf],
52
+ };
53
+ });
54
+ await (0, js_lib_1.pMap)(statements, async (statement) => {
55
+ if (this.cfg.debug)
56
+ this.db.cfg.logger.log(statement.sql);
57
+ await this.db.runSQL(statement);
58
+ });
59
+ }
60
+ streamIds(table, limit) {
61
+ let sql = `SELECT id FROM ${table}`;
62
+ if (limit)
63
+ sql += ` LIMIT ${limit}`;
64
+ if (this.cfg.logSQL)
65
+ this.db.cfg.logger.log(`stream: ${sql}`);
66
+ return this.db
67
+ .pool()
68
+ .query(sql)
69
+ .stream()
70
+ .pipe(new stream_1.Transform({
71
+ objectMode: true,
72
+ transform(row, _, cb) {
73
+ cb(null, row.id);
74
+ },
75
+ }));
76
+ }
77
+ streamValues(table, limit) {
78
+ let sql = `SELECT v FROM ${table}`;
79
+ if (limit)
80
+ sql += ` LIMIT ${limit}`;
81
+ if (this.cfg.logSQL)
82
+ this.db.cfg.logger.log(`stream: ${sql}`);
83
+ return this.db
84
+ .pool()
85
+ .query(sql)
86
+ .stream()
87
+ .pipe(new stream_1.Transform({
88
+ objectMode: true,
89
+ transform(row, _, cb) {
90
+ cb(null, row.v);
91
+ },
92
+ }));
93
+ }
94
+ streamEntries(table, limit) {
95
+ let sql = `SELECT id,v FROM ${table}`;
96
+ if (limit)
97
+ sql += ` LIMIT ${limit}`;
98
+ if (this.cfg.logSQL)
99
+ this.db.cfg.logger.log(`stream: ${sql}`);
100
+ return this.db
101
+ .pool()
102
+ .query(sql)
103
+ .stream()
104
+ .pipe(new stream_1.Transform({
105
+ objectMode: true,
106
+ transform(row, _, cb) {
107
+ cb(null, [row.id, row.v]);
108
+ },
109
+ }));
110
+ }
111
+ async beginTransaction() {
112
+ await this.db.runSQL({ sql: `BEGIN TRANSACTION` });
113
+ }
114
+ async endTransaction() {
115
+ await this.db.runSQL({ sql: `END TRANSACTION` });
116
+ }
117
+ async count(table) {
118
+ const sql = `SELECT count(*) as cnt FROM ${table}`;
119
+ if (this.cfg.logSQL)
120
+ this.db.cfg.logger.log(sql);
121
+ const rows = await this.db.runSQL({ sql });
122
+ return rows[0].cnt;
123
+ }
124
+ }
125
+ exports.MySQLKeyValueDB = MySQLKeyValueDB;
@@ -6,7 +6,7 @@ export declare function dbQueryToSQLDelete(q: DBQuery<any>): string;
6
6
  /**
7
7
  * Returns array of sql statements to respect the max sql size.
8
8
  */
9
- export declare function insertSQL(table: string, records: Record<any, any>[], verb?: 'INSERT' | 'REPLACE', logger?: CommonLogger): string[];
9
+ export declare function insertSQL(table: string, rows: Record<any, any>[], verb?: 'INSERT' | 'REPLACE', logger?: CommonLogger): string[];
10
10
  export declare function insertSQLSingle(table: string, record: Record<any, any>): string;
11
11
  export declare function insertSQLSetSingle(table: string, record: Record<any, any>): QueryOptions;
12
12
  export declare function dbQueryToSQLUpdate(q: DBQuery<any>, record: Record<any, any>): string;
@@ -12,7 +12,7 @@ function dbQueryToSQLSelect(q) {
12
12
  // filters
13
13
  tokens.push(...whereTokens(q));
14
14
  // order
15
- tokens.push(...orderTokens(q));
15
+ tokens.push(...groupOrderTokens(q));
16
16
  // offset/limit
17
17
  tokens.push(...offsetLimitTokens(q));
18
18
  return tokens.join(' ');
@@ -30,12 +30,12 @@ exports.dbQueryToSQLDelete = dbQueryToSQLDelete;
30
30
  /**
31
31
  * Returns array of sql statements to respect the max sql size.
32
32
  */
33
- function insertSQL(table, records, verb = 'INSERT', logger = console) {
33
+ function insertSQL(table, rows, verb = 'INSERT', logger = console) {
34
34
  // INSERT INTO table_name (column1, column2, column3, ...)
35
35
  // VALUES (value1, value2, value3, ...);
36
36
  // eslint-disable-next-line unicorn/no-array-reduce
37
- const fieldSet = records.reduce((set, rec) => {
38
- Object.keys(rec).forEach(field => set.add(field));
37
+ const fieldSet = rows.reduce((set, row) => {
38
+ Object.keys(row).forEach(field => set.add(field));
39
39
  return set;
40
40
  }, new Set());
41
41
  const fields = [...fieldSet];
@@ -46,7 +46,7 @@ function insertSQL(table, records, verb = 'INSERT', logger = console) {
46
46
  `(` + [...fields].map(f => mysql.escapeId((0, mysql_schema_util_1.mapNameToMySQL)(f))).join(',') + `)`,
47
47
  `VALUES\n`,
48
48
  ].join(' ');
49
- const valueRows = records.map(rec => {
49
+ const valueRows = rows.map(rec => {
50
50
  return `(` + fields.map(k => mysql.escape(rec[k])).join(',') + `)`;
51
51
  });
52
52
  const full = start + valueRows.join(',\n');
@@ -124,7 +124,13 @@ function selectTokens(q) {
124
124
  fields = q._selectedFieldNames.length ? q._selectedFieldNames : ['id'];
125
125
  }
126
126
  // We don't do `escapeId` cause it'll ruin e.g SELECT `count *` FROM ...
127
- return [`SELECT`, fields.map(f => (0, mysql_schema_util_1.mapNameToMySQL)(f)).join(', '), `FROM`, mysql.escapeId(q.table)];
127
+ return [
128
+ `SELECT`,
129
+ q._distinct && 'DISTINCT',
130
+ fields.map(f => (0, mysql_schema_util_1.mapNameToMySQL)(f)).join(', '),
131
+ `FROM`,
132
+ mysql.escapeId(q.table),
133
+ ].filter(Boolean);
128
134
  }
129
135
  function offsetLimitTokens(q) {
130
136
  const tokens = [];
@@ -137,13 +143,15 @@ function offsetLimitTokens(q) {
137
143
  }
138
144
  return tokens;
139
145
  }
140
- function orderTokens(q) {
141
- if (!q._orders.length)
142
- return [];
143
- return [
144
- `ORDER BY`,
145
- q._orders.map(o => `\`${o.name}\` ${o.descending ? 'DESC' : 'ASC'}`).join(', '),
146
- ];
146
+ function groupOrderTokens(q) {
147
+ const t = [];
148
+ if (q._groupByFieldNames?.length) {
149
+ t.push(`GROUP BY`, q._groupByFieldNames.map(c => `\`${c}\``).join(', '));
150
+ }
151
+ if (q._orders.length) {
152
+ t.push(`ORDER BY`, q._orders.map(o => `\`${o.name}\` ${o.descending ? 'DESC' : 'ASC'}`).join(', '));
153
+ }
154
+ return t;
147
155
  }
148
156
  const OP_MAP = {
149
157
  '==': '=',
@@ -80,7 +80,7 @@ function mysqlTableStatsToJsonSchemaField(table, stats, logger) {
80
80
  else if (t.startsWith('tinyint') || t.includes('(1)')) {
81
81
  s.properties[name] = { type: 'boolean' };
82
82
  }
83
- else if (t.startsWith('int(')) {
83
+ else if (t === 'int' || t.startsWith('int(')) {
84
84
  s.properties[name] = { type: 'integer' };
85
85
  }
86
86
  else if (t.startsWith('float')) {
package/package.json CHANGED
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "@naturalcycles/dev-lib": "^12.7.5",
15
- "@types/node": "^16.10.2",
16
- "dotenv": "^10.0.0",
15
+ "@types/node": "^17.0.5",
16
+ "dotenv": "^14.1.0",
17
17
  "jest": "^27.2.4"
18
18
  },
19
19
  "files": [
@@ -36,7 +36,7 @@
36
36
  "engines": {
37
37
  "node": ">=14.15.0"
38
38
  },
39
- "version": "3.9.0",
39
+ "version": "3.11.0",
40
40
  "description": "MySQL client implementing CommonDB interface",
41
41
  "keywords": [
42
42
  "mysql",
package/readme.md CHANGED
@@ -3,12 +3,33 @@
3
3
  > MySQL client implementing CommonDB interface
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@naturalcycles/mysql-lib/latest.svg)](https://www.npmjs.com/package/@naturalcycles/mysql-lib)
6
- [![](https://circleci.com/gh/NaturalCycles/mysql-lib.svg?style=shield&circle-token=123)](https://circleci.com/gh/NaturalCycles/mysql-lib)
7
6
  [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
8
7
 
9
- # Packaging
8
+ # Quick start
10
9
 
11
- - `engines.node >= 10.13`: Latest Node.js LTS
12
- - `main: dist/index.js`: commonjs, es2018
13
- - `types: dist/index.d.ts`: typescript types
14
- - `/src` folder with source `*.ts` files included
10
+ todo
11
+
12
+ # Develop
13
+
14
+ Install MySQL server+client on Mac:
15
+
16
+ ```shell
17
+
18
+ brew install mysql
19
+
20
+ # start
21
+
22
+ mysql.server start
23
+
24
+ # secure
25
+
26
+ mysql_secure_installation
27
+
28
+ # connect
29
+
30
+ mysql -uroot
31
+
32
+ # Default options are read from the following files in the given order:
33
+ # /etc/my.cnf /etc/mysql/my.cnf /opt/homebrew/etc/my.cnf ~/.my.cnf
34
+
35
+ ```
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getDBAdapter } from './dbAdapter'
2
2
  import { MysqlDB, MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions } from './mysql.db'
3
+ import { MySQLKeyValueDB } from './mysqlKeyValueDB'
3
4
  import { jsonSchemaToMySQLDDL } from './schema/mysql.schema.util'
4
5
 
5
6
  export type { MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions }
6
7
 
7
- export { MysqlDB, jsonSchemaToMySQLDDL, getDBAdapter }
8
+ export { MysqlDB, MySQLKeyValueDB, jsonSchemaToMySQLDDL, getDBAdapter }
package/src/mysql.db.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  RunQueryResult,
11
11
  } from '@naturalcycles/db-lib'
12
12
  import {
13
- _filterNullishValues,
13
+ _filterUndefinedValues,
14
14
  _mapKeys,
15
15
  _mapValues,
16
16
  _Memo,
@@ -136,7 +136,7 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
136
136
  async close(): Promise<void> {
137
137
  const pool = this.pool()
138
138
  // eslint-disable-next-line @typescript-eslint/await-thenable
139
- await promisify(pool.end.bind(pool))
139
+ await promisify(pool.end.bind(pool))()
140
140
  this.cfg.logger.log('closed')
141
141
  }
142
142
 
@@ -191,14 +191,24 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
191
191
  _opt: MysqlDBOptions = {},
192
192
  ): Promise<RunQueryResult<OUT>> {
193
193
  const sql = dbQueryToSQLSelect(q)
194
- const rows = await this.runSQL<ROW[]>({ sql })
194
+ const rows = (await this.runSQL<ROW[]>({ sql })).map(
195
+ row => _mapKeys(_filterUndefinedValues(row, true), k => mapNameFromMySQL(k)) as any,
196
+ )
197
+
198
+ // edge case where 0 fields are selected
199
+ if (q._selectedFieldNames?.length === 0) {
200
+ return {
201
+ rows: rows.map(_ => ({} as any)),
202
+ }
203
+ }
204
+
195
205
  return {
196
- rows: rows.map(r => _mapKeys(_filterNullishValues(r, true), k => mapNameFromMySQL(k)) as any),
206
+ rows,
197
207
  }
198
208
  }
199
209
 
200
210
  async runSQL<RESULT>(q: QueryOptions): Promise<RESULT> {
201
- if (this.cfg.logSQL) this.cfg.logger.log(q.sql, q.values)
211
+ if (this.cfg.logSQL) this.cfg.logger.log(...[q.sql, q.values].filter(Boolean))
202
212
 
203
213
  return await new Promise<RESULT>((resolve, reject) => {
204
214
  this.pool().query(q, (err, res) => {
@@ -208,6 +218,21 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
208
218
  })
209
219
  }
210
220
 
221
+ /**
222
+ * Allows to run semicolon-separated "SQL file".
223
+ * E.g "ddl reset script".
224
+ */
225
+ async runSQLString(s: string): Promise<void> {
226
+ const queries = s
227
+ .split(';')
228
+ .map(s => s.trim())
229
+ .filter(Boolean)
230
+
231
+ for await (const sql of queries) {
232
+ await this.runSQL({ sql })
233
+ }
234
+ }
235
+
211
236
  override async runQueryCount<ROW extends ObjectWithId>(
212
237
  q: DBQuery<ROW>,
213
238
  _opt?: CommonDBOptions,
@@ -232,7 +257,7 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
232
257
  new Transform({
233
258
  objectMode: true,
234
259
  transform(row, _encoding, cb) {
235
- cb(null, _filterNullishValues(row, true))
260
+ cb(null, _filterUndefinedValues(row, true))
236
261
  },
237
262
  }),
238
263
  )
@@ -248,13 +273,13 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
248
273
 
249
274
  // Stringify object values
250
275
  const rows = _rows.map(row =>
251
- _mapValues(row, v => {
276
+ _mapValues(row, (_k, v) => {
252
277
  return v && typeof v === 'object' && !Buffer.isBuffer(v) ? JSON.stringify(v) : v
253
278
  }),
254
279
  )
255
280
 
256
281
  // inserts are split into multiple sentenses to respect the max_packet_size (1Mb usually)
257
- const sqls = insertSQL(table, rows, 'INSERT', this.cfg.logger)
282
+ const sqls = insertSQL(table, rows, 'REPLACE', this.cfg.logger)
258
283
 
259
284
  for await (const sql of sqls) {
260
285
  await this.runSQL({ sql })
@@ -0,0 +1,148 @@
1
+ import { Transform } from 'stream'
2
+ import { CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib'
3
+ import { pMap } from '@naturalcycles/js-lib'
4
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib'
5
+ import { QueryOptions } from 'mysql'
6
+ import { MysqlDB, MysqlDBCfg } from './mysql.db'
7
+
8
+ interface KeyValueObject {
9
+ id: string
10
+ v: Buffer
11
+ }
12
+
13
+ export class MySQLKeyValueDB implements CommonKeyValueDB {
14
+ constructor(public cfg: MysqlDBCfg) {
15
+ this.db = new MysqlDB(this.cfg)
16
+ }
17
+
18
+ db: MysqlDB
19
+
20
+ async ping(): Promise<void> {
21
+ await this.db.ping()
22
+ }
23
+
24
+ async close(): Promise<void> {
25
+ await this.db.close()
26
+ }
27
+
28
+ async createTable(table: string, opt: CommonDBCreateOptions = {}): Promise<void> {
29
+ if (opt.dropIfExists) await this.dropTable(table)
30
+
31
+ // On blob sizes: https://tableplus.com/blog/2019/10/tinyblob-blob-mediumblob-longblob.html
32
+ // LONGBLOB supports up to 4gb
33
+ // MEDIUMBLOB up to 16Mb
34
+ const sql = `create table ${table} (id VARCHAR(64) PRIMARY KEY, v LONGBLOB NOT NULL)`
35
+ this.db.cfg.logger.log(sql)
36
+ await this.db.runSQL({ sql })
37
+ }
38
+
39
+ async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
40
+ if (!ids.length) return []
41
+
42
+ const sql = `SELECT id,v FROM ${table} where id in (${ids.map(id => `"${id}"`).join(',')})`
43
+
44
+ const rows = await this.db.runSQL<KeyValueObject[]>({ sql })
45
+
46
+ return rows.map(({ id, v }) => [id, v])
47
+ }
48
+
49
+ /**
50
+ * Use with caution!
51
+ */
52
+ async dropTable(table: string): Promise<void> {
53
+ await this.db.runSQL({ sql: `DROP TABLE IF EXISTS ${table}` })
54
+ }
55
+
56
+ async deleteByIds(table: string, ids: string[]): Promise<void> {
57
+ const sql = `DELETE FROM ${table} WHERE id in (${ids.map(id => `"${id}"`).join(',')})`
58
+ if (this.cfg.logSQL) this.db.cfg.logger.log(sql)
59
+ await this.db.runSQL({ sql })
60
+ }
61
+
62
+ async saveBatch(table: string, entries: KeyValueDBTuple[]): Promise<void> {
63
+ const statements: QueryOptions[] = entries.map(([id, buf]) => {
64
+ return {
65
+ sql: `INSERT INTO ${table} (id, v) VALUES (?, ?)`,
66
+ values: [id, buf],
67
+ }
68
+ })
69
+
70
+ await pMap(statements, async statement => {
71
+ if (this.cfg.debug) this.db.cfg.logger.log(statement.sql)
72
+ await this.db.runSQL(statement)
73
+ })
74
+ }
75
+
76
+ streamIds(table: string, limit?: number): ReadableTyped<string> {
77
+ let sql = `SELECT id FROM ${table}`
78
+ if (limit) sql += ` LIMIT ${limit}`
79
+ if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`)
80
+
81
+ return this.db
82
+ .pool()
83
+ .query(sql)
84
+ .stream()
85
+ .pipe(
86
+ new Transform({
87
+ objectMode: true,
88
+ transform(row, _, cb) {
89
+ cb(null, row.id)
90
+ },
91
+ }),
92
+ )
93
+ }
94
+
95
+ streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
96
+ let sql = `SELECT v FROM ${table}`
97
+ if (limit) sql += ` LIMIT ${limit}`
98
+ if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`)
99
+
100
+ return this.db
101
+ .pool()
102
+ .query(sql)
103
+ .stream()
104
+ .pipe(
105
+ new Transform({
106
+ objectMode: true,
107
+ transform(row, _, cb) {
108
+ cb(null, row.v)
109
+ },
110
+ }),
111
+ )
112
+ }
113
+
114
+ streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
115
+ let sql = `SELECT id,v FROM ${table}`
116
+ if (limit) sql += ` LIMIT ${limit}`
117
+ if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`)
118
+
119
+ return this.db
120
+ .pool()
121
+ .query(sql)
122
+ .stream()
123
+ .pipe(
124
+ new Transform({
125
+ objectMode: true,
126
+ transform(row, _, cb) {
127
+ cb(null, [row.id, row.v])
128
+ },
129
+ }),
130
+ )
131
+ }
132
+
133
+ async beginTransaction(): Promise<void> {
134
+ await this.db.runSQL({ sql: `BEGIN TRANSACTION` })
135
+ }
136
+
137
+ async endTransaction(): Promise<void> {
138
+ await this.db.runSQL({ sql: `END TRANSACTION` })
139
+ }
140
+
141
+ async count(table: string): Promise<number> {
142
+ const sql = `SELECT count(*) as cnt FROM ${table}`
143
+ if (this.cfg.logSQL) this.db.cfg.logger.log(sql)
144
+
145
+ const rows = await this.db.runSQL<{ cnt: number }[]>({ sql })
146
+ return rows[0]!.cnt
147
+ }
148
+ }
package/src/query.util.ts CHANGED
@@ -16,7 +16,7 @@ export function dbQueryToSQLSelect(q: DBQuery<any>): string {
16
16
  tokens.push(...whereTokens(q))
17
17
 
18
18
  // order
19
- tokens.push(...orderTokens(q))
19
+ tokens.push(...groupOrderTokens(q))
20
20
 
21
21
  // offset/limit
22
22
  tokens.push(...offsetLimitTokens(q))
@@ -41,7 +41,7 @@ export function dbQueryToSQLDelete(q: DBQuery<any>): string {
41
41
  */
42
42
  export function insertSQL(
43
43
  table: string,
44
- records: Record<any, any>[],
44
+ rows: Record<any, any>[],
45
45
  verb: 'INSERT' | 'REPLACE' = 'INSERT',
46
46
  logger: CommonLogger = console,
47
47
  ): string[] {
@@ -49,8 +49,8 @@ export function insertSQL(
49
49
  // VALUES (value1, value2, value3, ...);
50
50
 
51
51
  // eslint-disable-next-line unicorn/no-array-reduce
52
- const fieldSet = records.reduce((set: Set<string>, rec) => {
53
- Object.keys(rec).forEach(field => set.add(field))
52
+ const fieldSet = rows.reduce((set: Set<string>, row) => {
53
+ Object.keys(row).forEach(field => set.add(field))
54
54
  return set
55
55
  }, new Set<string>())
56
56
  const fields = [...fieldSet]
@@ -63,7 +63,7 @@ export function insertSQL(
63
63
  `VALUES\n`,
64
64
  ].join(' ')
65
65
 
66
- const valueRows = records.map(rec => {
66
+ const valueRows = rows.map(rec => {
67
67
  return `(` + fields.map(k => mysql.escape(rec[k])).join(',') + `)`
68
68
  })
69
69
 
@@ -155,7 +155,13 @@ function selectTokens(q: DBQuery): string[] {
155
155
  }
156
156
 
157
157
  // We don't do `escapeId` cause it'll ruin e.g SELECT `count *` FROM ...
158
- return [`SELECT`, fields.map(f => mapNameToMySQL(f)).join(', '), `FROM`, mysql.escapeId(q.table)]
158
+ return [
159
+ `SELECT`,
160
+ q._distinct && ('DISTINCT' as any),
161
+ fields.map(f => mapNameToMySQL(f)).join(', '),
162
+ `FROM`,
163
+ mysql.escapeId(q.table),
164
+ ].filter(Boolean)
159
165
  }
160
166
 
161
167
  function offsetLimitTokens(q: DBQuery<any>): string[] {
@@ -173,12 +179,21 @@ function offsetLimitTokens(q: DBQuery<any>): string[] {
173
179
  return tokens
174
180
  }
175
181
 
176
- function orderTokens(q: DBQuery): string[] {
177
- if (!q._orders.length) return []
178
- return [
179
- `ORDER BY`,
180
- q._orders.map(o => `\`${o.name}\` ${o.descending ? 'DESC' : 'ASC'}`).join(', '),
181
- ]
182
+ function groupOrderTokens(q: DBQuery): string[] {
183
+ const t: string[] = []
184
+
185
+ if (q._groupByFieldNames?.length) {
186
+ t.push(`GROUP BY`, q._groupByFieldNames.map(c => `\`${c}\``).join(', '))
187
+ }
188
+
189
+ if (q._orders.length) {
190
+ t.push(
191
+ `ORDER BY`,
192
+ q._orders.map(o => `\`${o.name}\` ${o.descending ? 'DESC' : 'ASC'}`).join(', '),
193
+ )
194
+ }
195
+
196
+ return t
182
197
  }
183
198
 
184
199
  const OP_MAP: Partial<Record<DBQueryFilterOperator, string>> = {
@@ -106,7 +106,7 @@ export function mysqlTableStatsToJsonSchemaField<T = any>(
106
106
  s.properties[name] = { instanceof: 'Buffer' }
107
107
  } else if (t.startsWith('tinyint') || t.includes('(1)')) {
108
108
  s.properties[name] = { type: 'boolean' } as JsonSchemaBoolean
109
- } else if (t.startsWith('int(')) {
109
+ } else if (t === 'int' || t.startsWith('int(')) {
110
110
  s.properties[name] = { type: 'integer' } as JsonSchemaNumber
111
111
  } else if (t.startsWith('float')) {
112
112
  s.properties[name] = { type: 'number' } as JsonSchemaNumber