@naturalcycles/mysql-lib 3.9.2 → 3.11.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.
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; } });
@@ -40,16 +40,21 @@ export declare class MysqlDB extends BaseCommonDB implements CommonDB {
40
40
  * Be careful to manage this connection yourself (open, release, etc).
41
41
  */
42
42
  createSingleConnection(): Promise<Connection>;
43
- getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: MysqlDBOptions): Promise<ROW[]>;
43
+ getByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], 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>;
49
54
  /**
50
55
  * Limitation: always returns [], regardless of which rows are actually deleted
51
56
  */
52
- deleteByIds(table: string, ids: string[], _opt?: MysqlDBOptions): Promise<number>;
57
+ deleteByIds<ROW extends ObjectWithId>(table: string, ids: ROW['id'][], _opt?: MysqlDBOptions): Promise<number>;
53
58
  deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
54
59
  /**
55
60
  * Use with caution!
package/dist/mysql.db.js CHANGED
@@ -138,6 +138,19 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
138
138
  });
139
139
  });
140
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
+ }
141
154
  async runQueryCount(q, _opt) {
142
155
  const { rows } = await this.runQuery(q.select(['count(*) as _count']));
143
156
  return rows[0]._count;
@@ -214,7 +227,7 @@ class MysqlDB extends db_lib_1.BaseCommonDB {
214
227
  return (0, mysql_schema_util_1.mysqlTableStatsToJsonSchemaField)(table, stats, this.cfg.logger);
215
228
  }
216
229
  }
217
- (0, tslib_1.__decorate)([
230
+ tslib_1.__decorate([
218
231
  (0, js_lib_1._Memo)()
219
232
  ], MysqlDB.prototype, "pool", null);
220
233
  exports.MysqlDB = MysqlDB;
@@ -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;
@@ -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(' ');
@@ -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
  '==': '=',
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "devDependencies": {
14
14
  "@naturalcycles/dev-lib": "^12.7.5",
15
15
  "@types/node": "^17.0.5",
16
- "dotenv": "^10.0.0",
16
+ "dotenv": "^16.0.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.2",
39
+ "version": "3.11.1",
40
40
  "description": "MySQL client implementing CommonDB interface",
41
41
  "keywords": [
42
42
  "mysql",
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
@@ -176,7 +176,7 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
176
176
  // GET
177
177
  override async getByIds<ROW extends ObjectWithId>(
178
178
  table: string,
179
- ids: string[],
179
+ ids: ROW['id'][],
180
180
  opt: MysqlDBOptions = {},
181
181
  ): Promise<ROW[]> {
182
182
  if (!ids.length) return []
@@ -218,6 +218,21 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
218
218
  })
219
219
  }
220
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
+
221
236
  override async runQueryCount<ROW extends ObjectWithId>(
222
237
  q: DBQuery<ROW>,
223
238
  _opt?: CommonDBOptions,
@@ -275,7 +290,11 @@ export class MysqlDB extends BaseCommonDB implements CommonDB {
275
290
  /**
276
291
  * Limitation: always returns [], regardless of which rows are actually deleted
277
292
  */
278
- override async deleteByIds(table: string, ids: string[], _opt?: MysqlDBOptions): Promise<number> {
293
+ override async deleteByIds<ROW extends ObjectWithId>(
294
+ table: string,
295
+ ids: ROW['id'][],
296
+ _opt?: MysqlDBOptions,
297
+ ): Promise<number> {
279
298
  if (!ids.length) return 0
280
299
  const sql = dbQueryToSQLDelete(new DBQuery(table).filterEq('id', ids))
281
300
  const res = await this.runSQL<any>({ 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))
@@ -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>> = {