leoric 1.11.1 → 1.12.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/History.md CHANGED
@@ -1,3 +1,10 @@
1
+ 1.12.0 / 2021-10-12
2
+ ===================
3
+
4
+ * feat: support custom fields query and sequelize mode export rawAttributes (#192)
5
+ * refactor: collection format query result (#194)
6
+ * refactor: object condition parsing and expression formatting (#191)
7
+
1
8
  1.11.1 / 2021-09-28
2
9
  ===================
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leoric",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "JavaScript Object-relational mapping alchemy",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "test:mysql2": "./test/start.sh test/integration/mysql2.test.js",
21
21
  "test:postgres": "./test/start.sh test/integration/postgres.test.js",
22
22
  "test:sqlite": "./test/start.sh test/integration/sqlite.test.js",
23
+ "test:dts": "./test/start.sh test/types/dts.test.js",
23
24
  "test:coverage": "nyc ./test/start.sh && nyc report --reporter=lcov",
24
25
  "lint": "eslint ./",
25
26
  "lint:fix": "eslint . --fix"
@@ -57,6 +58,8 @@
57
58
  "@babel/eslint-parser": "^7.14.7",
58
59
  "@cara/minami": "^1.2.3",
59
60
  "@journeyapps/sqlcipher": "^5.2.0",
61
+ "@types/node": "^16.10.1",
62
+ "coffee": "^5.4.0",
60
63
  "dayjs": "^1.10.3",
61
64
  "eslint": "^7.20.0",
62
65
  "expect.js": "^0.3.1",
@@ -67,6 +70,7 @@
67
70
  "nyc": "^15.1.0",
68
71
  "pg": "^8.5.1",
69
72
  "sinon": "^10.0.0",
70
- "sqlite3": "^5.0.2"
73
+ "sqlite3": "^5.0.2",
74
+ "typescript": "^4.4.3"
71
75
  }
72
76
  }
@@ -130,6 +130,10 @@ module.exports = Bone => {
130
130
  return this;
131
131
  }
132
132
 
133
+ static get rawAttributes() {
134
+ return this.attributes;
135
+ }
136
+
133
137
  /**
134
138
  * add scope see https://sequelize.org/master/class/lib/model.js~Model.html#static-method-addScope
135
139
  *
package/src/bone.js CHANGED
@@ -1147,19 +1147,30 @@ class Bone {
1147
1147
  * @return {Bone}
1148
1148
  */
1149
1149
  static instantiate(row) {
1150
- const { attributes, driver } = this;
1150
+ const { attributes, driver, attributeMap } = this;
1151
1151
  const instance = new this();
1152
1152
 
1153
- for (const name in attributes) {
1154
- const { columnName, jsType } = attributes[name];
1155
- if (columnName in row) {
1153
+ for (const columnName in row) {
1154
+ const value = row[columnName];
1155
+ if (attributeMap.hasOwnProperty(columnName)) {
1156
+ const { jsType, name } = attributeMap[columnName];
1156
1157
  // to make sure raw and rawSaved hold two different objects
1157
- instance._setRaw(name, driver.cast(row[columnName], jsType));
1158
- instance._setRawSaved(name, driver.cast(row[columnName], jsType));
1158
+ instance._setRaw(name, driver.cast(value, jsType));
1159
+ instance._setRawSaved(name, driver.cast(value, jsType));
1159
1160
  } else {
1161
+ if (!isNaN(value)) instance[columnName] = Number(value);
1162
+ else if (!isNaN(Date.parse(value))) instance[columnName] = new Date(value);
1163
+ else instance[columnName] = value;
1164
+ }
1165
+ }
1166
+
1167
+ for (const name in attributes) {
1168
+ const { columnName } = attributes[name];
1169
+ if (!(columnName in row)) {
1160
1170
  instance._getRawUnset().add(name);
1161
1171
  }
1162
1172
  }
1173
+
1163
1174
  instance.isNewRecord = false;
1164
1175
  return instance;
1165
1176
  }
package/src/collection.js CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const Spell = require('./spell');
4
-
5
3
  /**
6
4
  * An extended Array to represent collections of models.
7
5
  */
@@ -14,8 +12,7 @@ class Collection extends Array {
14
12
  * @returns {Collection|Array}
15
13
  */
16
14
  static init({ spell, rows, fields }) {
17
- if (isDispatchable(spell)) return dispatch(spell, rows, fields);
18
- return convert(spell, rows, fields);
15
+ return dispatch(spell, rows, fields);
19
16
  }
20
17
 
21
18
  /**
@@ -55,92 +52,98 @@ class Collection extends Array {
55
52
  }
56
53
 
57
54
  /**
58
- * Check if current spell is eligible to build models.
59
- * @param {Spell} spell
60
- * @returns {boolean}
55
+ * get all columns
56
+ * @param {Spell} spell
57
+ * @returns {Array<String>} columns
61
58
  */
62
- function isDispatchable(spell) {
63
- const { columns, groups, table } = spell;
64
-
65
- for (const token of columns) {
66
- const { type } = token;
67
- if (type === 'func') return false;
68
- if (type === 'alias' && token.args[0].type === 'func') return false;
69
- }
70
- if (groups.length > 0) return false;
71
- if (table.value instanceof Spell && Object.keys(table.value.joins).length > 0) {
72
- return false;
59
+ function getColumns(spell) {
60
+ const { joins, columns, Model } = spell;
61
+ const { tableAlias, table, driver } = Model;
62
+ if (!columns.length) return [];
63
+ let targetColumns = [];
64
+ const joined = Object.keys(joins).length > 0;
65
+ if (driver.type === 'sqlite' && joined) {
66
+ /**
67
+ * { type: 'alias', value: 'posts:id', args: [ { type: 'id', qualifiers: [ 'posts' ], value: 'ss' } ] },
68
+ */
69
+ if (columns.length) {
70
+ for (const column of columns) {
71
+ targetColumns.push(...column.args.filter(
72
+ c => c.qualifiers && (c.qualifiers.includes(table) || c.qualifiers.includes(tableAlias))
73
+ ).map(c => c.value));
74
+ }
75
+ }
76
+ } else {
77
+ targetColumns = columns.filter(c => !c.qualifiers || c.qualifiers.includes(table) || c.qualifiers.includes(tableAlias)).map(v => v.value);
73
78
  }
74
- return true;
79
+ return targetColumns;
75
80
  }
76
81
 
77
82
  /**
78
- * Convert the results to collection that consists of models with their associations set up according to `spell.joins`.
79
- * @private
80
- * @param {Spell} spell
81
- * @param {Object[]} rows
82
- * @param {Object[]} fields
83
- * @returns {Collection}
83
+ * join target instantiatable or not
84
+ * @param {Object} join
85
+ * @returns {boolean}
84
86
  */
85
- function dispatch(spell, rows, fields) {
86
- const { Model } = spell;
87
-
88
- if (Object.keys(spell.joins).length === 0) {
89
- return Collection.from(rows, row => Model.instantiate(Object.values(row)[0]));
90
- }
91
-
92
- const results = new Collection();
93
- const { tableAlias, table, primaryColumn, primaryKey } = Model;
94
-
95
- for (const row of rows) {
96
- // If SQL contains subqueries, such as `SELECT * FROM (SELECT * FROM foo) AS bar`,
97
- // the table name of the columns in SQLite is the original table name instead of the alias.
98
- // Hence we need to fallback to original table name here.
99
- const main = row[tableAlias] || row[table];
100
- let current = results.find(result => result[primaryKey] == main[primaryColumn]);
101
-
102
- if (!current) {
103
- current = Model.instantiate(main);
104
- results.push(current);
87
+ function joinedInstantiatable(join) {
88
+ const { on, Model } = join;
89
+ const { tableAlias, table } = Model;
90
+ let columns = [];
91
+ if (on && on.args && on.args.length) {
92
+ for (const arg of on.args) {
93
+ const { type } = arg;
94
+ // { type: 'op', ..., args: [{ value: '', qualifiers: [] }] }
95
+ if (type === 'op' && arg.args && arg.args.length) {
96
+ columns.push(...arg.args.filter(
97
+ c => c.qualifiers && (c.qualifiers.includes(table) || c.qualifiers.includes(tableAlias))
98
+ ).map(c => c.value));
99
+ } else if (arg.value && arg.qualifiers && (arg.qualifiers.includes(table) || arg.qualifiers.includes(tableAlias))) {
100
+ // { type: 'id', value: '', qualifiers: [] }
101
+ columns.push(arg.value);
102
+ }
105
103
  }
106
-
107
- dispatchJoins(current, spell, row, fields);
108
104
  }
105
+ if (!columns.length) return true;
106
+ const attributeKeys = Object.keys(Model.attributes);
107
+ return columns.some(r => attributeKeys.includes(r));
108
+ }
109
109
 
110
- return results;
110
+ /**
111
+ * @param {Spell} spell
112
+ * @returns duplicate main Model
113
+ */
114
+ function shouldFindJoinTarget(spell) {
115
+ const { Model } = spell;
116
+ const { primaryKey } = Model;
117
+ const columns = getColumns(spell);
118
+ return !columns.length || columns.length && columns.includes(primaryKey);
111
119
  }
112
120
 
113
- function dispatchJoins(current, spell, row, fields) {
114
- for (const qualifier in spell.joins) {
115
- const { Model, hasMany } = spell.joins[qualifier];
116
- // mysql2 nests rows with table name instead of table alias.
117
- const values = row[qualifier] || row[Model.table];
118
- const id = values[Model.primaryColumn];
119
- if (hasMany) {
120
- if (!current[qualifier]) current[qualifier] = new Collection();
121
- if (!id || current[qualifier].some(item => item[Model.primaryKey] === id)) continue;
122
- current[qualifier].push(Model.instantiate(values));
123
- } else {
124
- current[qualifier] = Object.values(values).some(value => value != null)
125
- ? Model.instantiate(values)
126
- : null;
127
- }
128
- }
121
+ /**
122
+ * @param {Spell} spell
123
+ * @returns {boolean}
124
+ */
125
+ function instantiatable(spell) {
126
+ const { Model, groups } = spell;
127
+ const { attributes } = Model;
128
+ const columns = getColumns(spell);
129
+ if (groups.length > 0) return false;
130
+ if (!columns.length) return true;
131
+ const attributeKeys = Object.keys(attributes);
132
+ return columns.some(r => attributeKeys.includes(r));
129
133
  }
130
134
 
135
+
131
136
  /**
132
- * Convert returned rows to result set by translating columns (if found) to attributes.
137
+ * Convert the results to collection that consists of models with their associations set up according to `spell.joins`.
133
138
  * @private
134
139
  * @param {Spell} spell
135
140
  * @param {Object[]} rows
136
141
  * @param {Object[]} fields
137
- * @returns {Object[]}
142
+ * @returns {Collection}
138
143
  */
139
- function convert(spell, rows, fields) {
140
- const results = [];
141
- const { groups, joins, columns } = spell;
142
- const { table, tableAlias } = spell.Model;
143
-
144
+ function dispatch(spell, rows, fields) {
145
+ const { groups, joins, columns, Model } = spell;
146
+ const { tableAlias, table, primaryKey, primaryColumn, attributes } = Model;
144
147
  // await Post.count()
145
148
  if (rows.length <= 1 && columns.length === 1 && groups.length === 0) {
146
149
  const { type, value, args } = columns[0];
@@ -151,40 +154,69 @@ function convert(spell, rows, fields) {
151
154
  }
152
155
  }
153
156
 
157
+ const joined = Object.keys(joins).length > 0;
158
+ const shouldFindDuplicate = shouldFindJoinTarget(spell);
159
+ const canInstantiate = instantiatable(spell);
160
+ const attributeKeys = Object.keys(attributes);
161
+
162
+ const results = new Collection();
154
163
  for (const row of rows) {
155
- const result = { '': {} };
156
- for (let prop in row) {
164
+ const result = {};
165
+ for (const prop in row) {
157
166
  const data = row[prop];
158
- // mysql2 sometimes nests rows with table name instead of table alias
159
167
  const qualifier = prop === table ? tableAlias : prop;
160
- const obj = result[qualifier] || (result[qualifier] = {});
161
- if (qualifier === '') {
162
- Object.assign(obj, data);
168
+ if (qualifier === '' || qualifier === tableAlias) {
169
+ Object.assign(result, data);
170
+ } else {
171
+ if (Object.values(data).some(value => value != null)) result[prop] = data;
163
172
  }
164
- else if (qualifier in joins || qualifier == tableAlias) {
165
- const { Model } = joins[qualifier] || spell;
166
- for (const columnName in data) {
167
- const definition = Model.attributeMap[columnName];
168
- if (definition) {
169
- obj[definition.name] = data[columnName];
170
- } else {
171
- result[''][columnName] = data[columnName];
173
+ }
174
+ let current;
175
+ if (shouldFindDuplicate && result[primaryColumn] != null) {
176
+ current = results.find(r => r[primaryKey] == result[primaryColumn]);
177
+ }
178
+ if (!current) {
179
+ const resultKeys = Object.keys(result);
180
+ current = canInstantiate || (!groups.length && resultKeys.some(c => attributeKeys.includes(c)))? Model.instantiate(result) : result;
181
+ results.push(current);
182
+ }
183
+ if (joined) {
184
+ dispatchJoins(current, spell, row, fields);
185
+ }
186
+ }
187
+
188
+ return results;
189
+ }
190
+
191
+ function dispatchJoins(current, spell, row, fields) {
192
+ const instantiatableMap = {};
193
+ for (const qualifier in spell.joins) {
194
+ const join = spell.joins[qualifier];
195
+ const { Model, hasMany } = join;
196
+ if (instantiatableMap[qualifier] === undefined) instantiatableMap[qualifier] = joinedInstantiatable(join);
197
+ const joinInstantiatable = instantiatableMap[qualifier];
198
+ // mysql2 nests rows with table name instead of table alias.
199
+ const values = row[qualifier] || row[Model.table];
200
+ if (values) {
201
+ if (hasMany) {
202
+ const id = values[Model.primaryColumn];
203
+ if (!current[qualifier]) current[qualifier] = new Collection();
204
+ if (!Array.isArray(current[qualifier])) {
205
+ const origin = !(current[qualifier] instanceof Model) && joinInstantiatable? Model.instantiate(current[qualifier]) : current[qualifier];
206
+ current[qualifier] = new Collection();
207
+ if (Object.values(values).some(value => value != null)) {
208
+ current[qualifier].push(origin);
172
209
  }
173
210
  }
174
- }
175
- else {
176
- throw new Error(`Unknown qualifier ${qualifier}`);
211
+ if (!id || current[qualifier].some(item => item[Model.primaryKey] === id) || Object.values(values).every(value => value == null)) continue;
212
+ current[qualifier].push(joinInstantiatable? Model.instantiate(values) : values);
213
+ } else {
214
+ current[qualifier] = Object.values(values).some(value => value != null)
215
+ ? (joinInstantiatable? Model.instantiate(values) : values)
216
+ : null;
177
217
  }
178
218
  }
179
- results.push(result);
180
219
  }
181
-
182
- if (Object.keys(joins).length > 0) return results;
183
- return results.map(result => {
184
- const merged = {};
185
- for (const obj of Object.values(result)) Object.assign(merged, obj);
186
- return merged;
187
- });
188
220
  }
189
221
 
190
222
  module.exports = Collection;
@@ -2,25 +2,8 @@
2
2
 
3
3
  const SqlString = require('sqlstring');
4
4
 
5
- const { precedes, copyExpr, findExpr, walkExpr } = require('../../expr');
6
-
7
- /**
8
- * Find model by qualifiers.
9
- * @example
10
- * findModel(spell, ['comments'])
11
- * findModel(spell)
12
- *
13
- * @param {Spell} spell
14
- * @param {string[]} qualifiers
15
- */
16
- function findModel(spell, qualifiers) {
17
- const qualifier = qualifiers && qualifiers[0];
18
- const Model = qualifier && qualifier != spell.Model.tableAlias
19
- ? (spell.joins.hasOwnProperty(qualifier) ? spell.joins[qualifier].Model : null)
20
- : spell.Model;
21
- if (!Model) throw new Error(`Unabled to find model ${qualifiers}`);
22
- return Model;
23
- }
5
+ const { copyExpr, findExpr, walkExpr } = require('../../expr');
6
+ const { formatExpr, formatConditions, collectLiteral } = require('../../expr_formatter');
24
7
 
25
8
  /**
26
9
  * Format orders into ORDER BY clause in SQL
@@ -29,185 +12,11 @@ function findModel(spell, qualifiers) {
29
12
  */
30
13
  function formatOrders(spell, orders) {
31
14
  return orders.map(([token, order]) => {
32
- const column = formatColumn(spell, token);
15
+ const column = formatExpr(spell, token);
33
16
  return order == 'desc' ? `${column} DESC` : column;
34
17
  });
35
18
  }
36
19
 
37
- /**
38
- * Format token into identifiers/functions/etc. in SQL
39
- * @example
40
- * formatColumn(spell, { type: 'id', value: 'title' })
41
- * // => `title`
42
- *
43
- * formatColumn(spell, { type: 'func', name: 'year', args: [ { type: 'id', value: 'createdAt' } ] })
44
- * // => YEAR(`createdAt`)
45
- *
46
- * @param {Spell} spell
47
- * @param {Object} token
48
- */
49
- function formatColumn(spell, token) {
50
- if (token.type == 'id') {
51
- return formatIdentifier(spell, token);
52
- } else {
53
- return formatExpr(spell, token);
54
- }
55
- }
56
-
57
- /**
58
- * Format identifiers into escaped string with qualifiers.
59
- * @param {Spell} spell
60
- * @param {Object} ast
61
- */
62
- function formatIdentifier(spell, ast) {
63
- const { value, qualifiers } = ast;
64
- const Model = findModel(spell, qualifiers);
65
- const column = Model.unalias(value);
66
- const { escapeId } = spell.Model.driver;
67
-
68
- if (qualifiers && qualifiers.length > 0) {
69
- return `${qualifiers.map(escapeId).join('.')}.${escapeId(column)}`;
70
- } else {
71
- return escapeId(column);
72
- }
73
- }
74
-
75
- const extractFieldNames = ['year', 'month', 'day'];
76
-
77
- function formatFuncExpr(spell, ast) {
78
- const { name, args } = ast;
79
- const { type } = spell.Model.driver;
80
-
81
- // https://www.postgresql.org/docs/9.1/static/functions-datetime.html
82
- if (type === 'postgres' && extractFieldNames.includes(name)) {
83
- return `EXTRACT(${name.toUpperCase()} FROM ${args.map(arg => formatExpr(spell, arg)).join(', ')})`;
84
- }
85
-
86
- return `${name.toUpperCase()}(${args.map(arg => formatExpr(spell, arg)).join(', ')})`;
87
- }
88
-
89
- /**
90
- * The `... IS NULL` predicate is not parameterizable.
91
- * - https://github.com/brianc/node-postgres/issues/1751
92
- * @param {Array} values the collected values
93
- * @param {Object} ast the abstract syntax tree
94
- * @returns {Array} values
95
- */
96
- function collectLiteral(values, ast) {
97
- walkExpr(ast, ({ type, value }) => {
98
- if (type == 'literal' && value != null) {
99
- if (Array.isArray(value)) {
100
- values.push(...value);
101
- } else {
102
- values.push(value);
103
- }
104
- }
105
- });
106
- return values;
107
- }
108
-
109
- function formatLiteral(spell, ast) {
110
- const { value } = ast;
111
-
112
- if (value == null) {
113
- return 'NULL';
114
- } else if (Array.isArray(value)) {
115
- if (value.length) return `(${value.map(() => '?').join(', ')})`;
116
- return '(NULL)';
117
- } else {
118
- return '?';
119
- }
120
- }
121
-
122
- /**
123
- * Format the abstract syntax tree of an expression into escaped string.
124
- * @param {Spell} spell
125
- * @param {Object} ast
126
- */
127
- function formatExpr(spell, ast) {
128
- const { type, name, value, args } = ast;
129
- switch (type) {
130
- case 'literal':
131
- return formatLiteral(spell, ast);
132
- case 'subquery':
133
- return `(${value.toSqlString()})`;
134
- case 'wildcard':
135
- return '*';
136
- case 'alias':
137
- return `${formatExpr(spell, args[0])} AS ${formatIdentifier(spell, ast)}`;
138
- case 'mod':
139
- return `${name.to.toUpperCase()} ${formatExpr(spell, args[0])}`;
140
- case 'id':
141
- return formatIdentifier(spell, ast);
142
- case 'op':
143
- return formatOpExpr(spell, ast);
144
- case 'func':
145
- return formatFuncExpr(spell, ast);
146
- case 'raw':
147
- // return value directly
148
- return value;
149
- default:
150
- throw new Error(`Unexpected type ${type}`);
151
- }
152
- }
153
-
154
- /**
155
- * Check if current token is logical operator or not, e.g. `AND`/`NOT`/`OR`.
156
- * @param {Object} ast
157
- */
158
- function isLogicalOp({ type, name }) {
159
- return type == 'op' && ['and', 'not', 'or'].includes(name);
160
- }
161
-
162
- /**
163
- * Format `{ type: 'op' }` expressions into escaped string.
164
- * @param {Spell} spell
165
- * @param {Object} ast
166
- */
167
- function formatOpExpr(spell, ast) {
168
- const { name, args } = ast;
169
- const params = args.map(arg => {
170
- return isLogicalOp(ast) && isLogicalOp(arg) && precedes(name, arg.name) < 0
171
- ? `(${formatExpr(spell, arg)})`
172
- : formatExpr(spell, arg);
173
- });
174
-
175
- if (name == 'between' || name == 'not between') {
176
- return `${params[0]} ${name.toUpperCase()} ${params[1]} AND ${params[2]}`;
177
- }
178
- else if (name == 'not') {
179
- return `NOT ${params[0]}`;
180
- }
181
- else if ('!~-'.includes(name) && params.length == 1) {
182
- return `${name} ${params[0]}`;
183
- }
184
- else if (args[1].type == 'literal' && args[1].value == null && !isLogicalOp(ast)) {
185
- if (['=', '!='].includes(name)) {
186
- const op = name == '=' ? 'IS' : 'IS NOT';
187
- return `${params[0]} ${op} NULL`;
188
- } else {
189
- throw new Error(`Invalid operator ${name} against null`);
190
- }
191
- }
192
- // IN (1, 2, 3)
193
- // IN (SELECT user_id FROM group_users)
194
- else if ((args[1].type == 'literal' && Array.isArray(args[1].value)) || args[1].type == 'subquery') {
195
- let op = name;
196
- if (name == '=') {
197
- op = 'in';
198
- } else if (name == '!=') {
199
- op = 'not in';
200
- }
201
- if (['in', 'not in'].includes(op)) {
202
- return `${params[0]} ${op.toUpperCase()} ${params[1]}`;
203
- } else {
204
- throw new Error(`Invalid operator ${name} against ${args[1].value}`);
205
- }
206
- } else if (params[1] !== '') {
207
- return `${params[0]} ${name.toUpperCase()} ${params[1]}`;
208
- }
209
- }
210
-
211
20
  /**
212
21
  * Format a spell without joins into a full SELECT query. This function is also used to format the subquery which is then used as a drived table in a SELECT with joins.
213
22
  * @param {Spell} spell
@@ -228,7 +37,7 @@ function formatSelectWithoutJoin(spell) {
228
37
  columns.reduce(collectLiteral, values);
229
38
  const selects = [];
230
39
  for (const token of columns) {
231
- const column = formatColumn(spell, token);
40
+ const column = formatExpr(spell, token);
232
41
  if (!selects.includes(column)) selects.push(column);
233
42
  }
234
43
  chunks.push(`${selects.join(', ')}`);
@@ -254,7 +63,7 @@ function formatSelectWithoutJoin(spell) {
254
63
  }
255
64
 
256
65
  if (groups.length > 0) {
257
- const groupColumns = groups.map(group => formatColumn(spell, group));
66
+ const groupColumns = groups.map(group => formatExpr(spell, group));
258
67
  chunks.push(`GROUP BY ${groupColumns.join(', ')}`);
259
68
  }
260
69
 
@@ -371,7 +180,7 @@ function formatSelectExpr(spell, values) {
371
180
 
372
181
  for (const token of columns) {
373
182
  collectLiteral(values, token);
374
- const selectExpr = formatColumn(spell, token);
183
+ const selectExpr = formatExpr(spell, token);
375
184
  const qualifier = token.qualifiers ? token.qualifiers[0] : '';
376
185
  const list = map[qualifier] || (map[qualifier] = []);
377
186
  list.push(selectExpr);
@@ -444,7 +253,7 @@ function formatSelectWithJoin(spell) {
444
253
  }
445
254
 
446
255
  if (groups.length > 0) {
447
- chunks.push(`GROUP BY ${groups.map(group => formatColumn(spell, group)).join(', ')}`);
256
+ chunks.push(`GROUP BY ${groups.map(group => formatExpr(spell, group)).join(', ')}`);
448
257
  }
449
258
 
450
259
  if (havingConditions.length > 0) {
@@ -513,22 +322,6 @@ function formatDelete(spell) {
513
322
  }
514
323
  }
515
324
 
516
- /**
517
- * Format an array of conditions into an expression. Conditions will be joined with `AND`.
518
- * @param {Object[]} conditions - An array of parsed where/having/on conditions
519
- */
520
- function formatConditions(spell, conditions) {
521
- return conditions
522
- .map(condition => {
523
- return isLogicalOp(condition) && condition.name == 'or' && conditions.length > 1
524
- ? `(${formatExpr(spell, condition)})`
525
- : formatExpr(spell, condition);
526
- })
527
- // filter empty condition
528
- .filter((condition) => !!condition)
529
- .join(' AND ');
530
- }
531
-
532
325
  /**
533
326
  * Format a spell into INSERT query.
534
327
  * @param {Spell} spell