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 +7 -0
- package/package.json +6 -2
- package/src/adapters/sequelize.js +4 -0
- package/src/bone.js +17 -6
- package/src/collection.js +128 -96
- package/src/drivers/abstract/spellbook.js +7 -214
- package/src/expr_formatter.js +192 -0
- package/src/hint.js +3 -3
- package/src/query_object.js +229 -0
- package/src/spell.js +10 -264
- package/types/index.d.ts +22 -0
- package/types/leoric-tests.ts +0 -25
- package/types/tsconfig.json +0 -5
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.
|
|
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
|
}
|
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
|
|
1154
|
-
const
|
|
1155
|
-
if (columnName
|
|
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(
|
|
1158
|
-
instance._setRawSaved(name, driver.cast(
|
|
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
|
-
|
|
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
|
-
*
|
|
59
|
-
* @param {Spell} spell
|
|
60
|
-
* @returns {
|
|
55
|
+
* get all columns
|
|
56
|
+
* @param {Spell} spell
|
|
57
|
+
* @returns {Array<String>} columns
|
|
61
58
|
*/
|
|
62
|
-
function
|
|
63
|
-
const {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
79
|
+
return targetColumns;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
/**
|
|
78
|
-
*
|
|
79
|
-
* @
|
|
80
|
-
* @
|
|
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
|
|
86
|
-
const { Model } =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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 {
|
|
142
|
+
* @returns {Collection}
|
|
138
143
|
*/
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
const {
|
|
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 (
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =>
|
|
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 =
|
|
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 =>
|
|
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
|