@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 +2 -1
- package/dist/index.js +3 -1
- package/dist/mysql.db.d.ts +5 -0
- package/dist/mysql.db.js +26 -7
- package/dist/mysqlKeyValueDB.d.ts +25 -0
- package/dist/mysqlKeyValueDB.js +125 -0
- package/dist/query.util.d.ts +1 -1
- package/dist/query.util.js +21 -13
- package/dist/schema/mysql.schema.util.js +1 -1
- package/package.json +3 -3
- package/readme.md +27 -6
- package/src/index.ts +2 -1
- package/src/mysql.db.ts +33 -8
- package/src/mysqlKeyValueDB.ts +148 -0
- package/src/query.util.ts +27 -12
- package/src/schema/mysql.schema.util.ts +1 -1
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; } });
|
package/dist/mysql.db.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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, '
|
|
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;
|
package/dist/query.util.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/query.util.js
CHANGED
|
@@ -12,7 +12,7 @@ function dbQueryToSQLSelect(q) {
|
|
|
12
12
|
// filters
|
|
13
13
|
tokens.push(...whereTokens(q));
|
|
14
14
|
// order
|
|
15
|
-
tokens.push(...
|
|
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,
|
|
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 =
|
|
38
|
-
Object.keys(
|
|
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 =
|
|
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 [
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
"dotenv": "^
|
|
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.
|
|
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
|
[](https://www.npmjs.com/package/@naturalcycles/mysql-lib)
|
|
6
|
-
[](https://circleci.com/gh/NaturalCycles/mysql-lib)
|
|
7
6
|
[](https://github.com/prettier/prettier)
|
|
8
7
|
|
|
9
|
-
#
|
|
8
|
+
# Quick start
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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, '
|
|
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(...
|
|
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
|
-
|
|
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 =
|
|
53
|
-
Object.keys(
|
|
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 =
|
|
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 [
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
q.
|
|
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
|