leoric 2.3.2 → 2.4.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 +13 -0
- package/Readme.md +1 -1
- package/index.js +7 -0
- package/package.json +3 -1
- package/src/bone.js +7 -8
- package/src/drivers/abstract/index.js +192 -1
- package/src/drivers/abstract/spellbook.js +403 -412
- package/src/drivers/index.js +15 -4
- package/src/drivers/mysql/index.js +101 -10
- package/src/drivers/mysql/spellbook.js +13 -11
- package/src/drivers/postgres/index.js +103 -109
- package/src/drivers/postgres/spellbook.js +9 -9
- package/src/drivers/postgres/sqlstring.js +124 -0
- package/src/drivers/sqlite/index.js +124 -13
- package/src/drivers/sqlite/spellbook.js +6 -6
- package/src/drivers/sqlite/sqlstring.js +88 -0
- package/src/hint.js +2 -1
- package/src/realm.js +7 -5
- package/src/spell.js +2 -4
- package/types/hint.d.ts +96 -0
- package/types/index.d.ts +246 -20
- package/src/drivers/abstract/schema.js +0 -143
- package/src/drivers/mysql/schema.js +0 -98
- package/src/drivers/postgres/schema.js +0 -125
- package/src/drivers/sqlite/schema.js +0 -211
|
@@ -6,84 +6,6 @@ const { copyExpr, findExpr, walkExpr } = require('../../expr');
|
|
|
6
6
|
const { formatExpr, formatConditions, collectLiteral } = require('../../expr_formatter');
|
|
7
7
|
const Raw = require('../../raw');
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Format orders into ORDER BY clause in SQL
|
|
11
|
-
* @param {Spell} spell
|
|
12
|
-
* @param {Object[]} orders
|
|
13
|
-
*/
|
|
14
|
-
function formatOrders(spell, orders) {
|
|
15
|
-
return orders.map(([token, order]) => {
|
|
16
|
-
const column = formatExpr(spell, token);
|
|
17
|
-
return order == 'desc' ? `${column} DESC` : column;
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 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.
|
|
23
|
-
* @param {Spell} spell
|
|
24
|
-
*/
|
|
25
|
-
function formatSelectWithoutJoin(spell) {
|
|
26
|
-
const { columns, whereConditions, groups, havingConditions, orders, rowCount, skip } = spell;
|
|
27
|
-
const chunks = ['SELECT'];
|
|
28
|
-
const values = [];
|
|
29
|
-
|
|
30
|
-
// see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
31
|
-
const hintStr = this.formatOptimizerHints(spell);
|
|
32
|
-
|
|
33
|
-
if (hintStr) {
|
|
34
|
-
chunks.push(hintStr);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (columns.length > 0) {
|
|
38
|
-
for (const column of columns) collectLiteral(spell, column, values);
|
|
39
|
-
const selects = [];
|
|
40
|
-
for (const token of columns) {
|
|
41
|
-
const column = formatExpr(spell, token);
|
|
42
|
-
if (!selects.includes(column)) selects.push(column);
|
|
43
|
-
}
|
|
44
|
-
chunks.push(`${selects.join(', ')}`);
|
|
45
|
-
} else {
|
|
46
|
-
chunks.push('*');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const table = formatExpr(spell, spell.table);
|
|
50
|
-
chunks.push(`FROM ${table}`);
|
|
51
|
-
if (spell.table.value instanceof spell.constructor) {
|
|
52
|
-
chunks.push(`AS t${spell.subqueryIndex++}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// see https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
|
|
56
|
-
const indexHintStr = this.formatIndexHints(spell);
|
|
57
|
-
if (indexHintStr) {
|
|
58
|
-
chunks.push(indexHintStr);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (whereConditions.length > 0) {
|
|
62
|
-
for (const condition of whereConditions) collectLiteral(spell, condition, values);
|
|
63
|
-
chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (groups.length > 0) {
|
|
67
|
-
const groupColumns = groups.map(group => formatExpr(spell, group));
|
|
68
|
-
chunks.push(`GROUP BY ${groupColumns.join(', ')}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (havingConditions.length > 0) {
|
|
72
|
-
for (const condition of havingConditions) collectLiteral(spell, condition, values);
|
|
73
|
-
chunks.push(`HAVING ${formatConditions(spell, havingConditions)}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (orders.length > 0) {
|
|
77
|
-
// ORDER BY FIND_IN_SET(`id`, '1,2,3')
|
|
78
|
-
for (const [ expr ] of orders) collectLiteral(spell, expr, values);
|
|
79
|
-
chunks.push(`ORDER BY ${formatOrders(spell, orders).join(', ')}`);
|
|
80
|
-
}
|
|
81
|
-
if (rowCount > 0) chunks.push(`LIMIT ${rowCount}`);
|
|
82
|
-
if (skip > 0) chunks.push(`OFFSET ${skip}`);
|
|
83
|
-
|
|
84
|
-
return { sql: chunks.join(' '), values };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
9
|
/**
|
|
88
10
|
* Create a subquery to make sure OFFSET and LIMIT on left table takes effect.
|
|
89
11
|
* @param {Spell} spell
|
|
@@ -194,416 +116,485 @@ function formatSelectExpr(spell, values) {
|
|
|
194
116
|
return Array.from(selects);
|
|
195
117
|
}
|
|
196
118
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const hintStr = this.formatOptimizerHints(spell);
|
|
215
|
-
|
|
216
|
-
if (hintStr) {
|
|
217
|
-
chunks.push(hintStr);
|
|
218
|
-
}
|
|
219
|
-
chunks.push(selects.join(', '));
|
|
220
|
-
|
|
221
|
-
let hoistable = skip > 0 || rowCount > 0;
|
|
222
|
-
if (hoistable) {
|
|
223
|
-
function checkQualifier({ type, qualifiers = [] }) {
|
|
224
|
-
if (type === 'id' && qualifiers.length> 0 && !qualifiers.includes(baseName)) {
|
|
225
|
-
hoistable = false;
|
|
226
|
-
}
|
|
119
|
+
class SpellBook {
|
|
120
|
+
format(spell) {
|
|
121
|
+
for (const scope of spell.scopes) scope(spell);
|
|
122
|
+
switch (spell.command) {
|
|
123
|
+
case 'insert':
|
|
124
|
+
case 'bulkInsert':
|
|
125
|
+
return this.formatInsert(spell);
|
|
126
|
+
case 'select':
|
|
127
|
+
return this.formatSelect(spell);
|
|
128
|
+
case 'update':
|
|
129
|
+
return this.formatUpdate(spell);
|
|
130
|
+
case 'delete':
|
|
131
|
+
return this.formatDelete(spell);
|
|
132
|
+
case 'upsert':
|
|
133
|
+
return this.formatUpsert(spell);
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`Unsupported SQL command ${spell.command}`);
|
|
227
136
|
}
|
|
228
|
-
for (const condition of whereConditions) walkExpr(condition, checkQualifier);
|
|
229
|
-
for (const orderExpr of orders) walkExpr(orderExpr[0], checkQualifier);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (hoistable) {
|
|
233
|
-
const subspell = createSubspell(spell);
|
|
234
|
-
const subquery = this.formatSelectWithoutJoin(subspell);
|
|
235
|
-
values.push(...subquery.values);
|
|
236
|
-
chunks.push(`FROM (${subquery.sql}) AS ${escapeId(baseName)}`);
|
|
237
|
-
} else {
|
|
238
|
-
chunks.push(`FROM ${escapeId(Model.table)} AS ${escapeId(baseName)}`);
|
|
239
137
|
}
|
|
240
138
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
139
|
+
/**
|
|
140
|
+
* @abstract
|
|
141
|
+
* @returns {string} optimizer hints
|
|
142
|
+
*/
|
|
143
|
+
formatOptimizerHints() {
|
|
144
|
+
return '';
|
|
245
145
|
}
|
|
246
146
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
147
|
+
/**
|
|
148
|
+
* @abstract
|
|
149
|
+
* @returns {string} index hints
|
|
150
|
+
*/
|
|
151
|
+
formatIndexHints() {
|
|
152
|
+
return '';
|
|
251
153
|
}
|
|
252
154
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Format a spell into INSERT query.
|
|
157
|
+
* @param {Spell} spell
|
|
158
|
+
*/
|
|
159
|
+
formatInsert(spell) {
|
|
160
|
+
const { Model, sets, columnAttributes: optAttrs, updateOnDuplicate } = spell;
|
|
161
|
+
const { shardingKey } = Model;
|
|
162
|
+
const { createdAt } = Model.timestamps;
|
|
163
|
+
const { escapeId } = Model.driver;
|
|
164
|
+
let columns = [];
|
|
165
|
+
let updateOnDuplicateColumns = [];
|
|
166
|
+
|
|
167
|
+
let values = [];
|
|
168
|
+
let placeholders = [];
|
|
169
|
+
if (Array.isArray(sets)) {
|
|
170
|
+
// merge records to get the big picture of involved columnAttributes
|
|
171
|
+
const involved = sets.reduce((result, entry) => {
|
|
172
|
+
return Object.assign(result, entry);
|
|
173
|
+
}, {});
|
|
174
|
+
const columnAttributes = [];
|
|
175
|
+
if (optAttrs) {
|
|
176
|
+
for (const name in optAttrs) {
|
|
177
|
+
if (involved.hasOwnProperty(name)) columnAttributes.push(columnAttributes[name]);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
for (const name in involved) {
|
|
181
|
+
columnAttributes.push(Model.columnAttributes[name]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const entry of columnAttributes) {
|
|
186
|
+
columns.push(entry.columnName);
|
|
187
|
+
if (updateOnDuplicate && createdAt && entry.name === createdAt
|
|
188
|
+
&& !(Array.isArray(updateOnDuplicate) && updateOnDuplicate.includes(createdAt))) continue;
|
|
189
|
+
updateOnDuplicateColumns.push(entry.columnName);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const entry of sets) {
|
|
193
|
+
if (shardingKey && entry[shardingKey] == null) {
|
|
194
|
+
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL.`);
|
|
195
|
+
}
|
|
196
|
+
for (const attribute of columnAttributes) {
|
|
197
|
+
const { name } = attribute;
|
|
198
|
+
values.push(entry[name]);
|
|
199
|
+
}
|
|
200
|
+
placeholders.push(`(${new Array(columnAttributes.length).fill('?').join(',')})`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
} else {
|
|
204
|
+
if (shardingKey && sets[shardingKey] == null) {
|
|
205
|
+
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL.`);
|
|
206
|
+
}
|
|
207
|
+
for (const name in sets) {
|
|
208
|
+
const value = sets[name];
|
|
209
|
+
columns.push(Model.unalias(name));
|
|
210
|
+
if (value instanceof Raw) {
|
|
211
|
+
values.push(SqlString.raw(value.value));
|
|
212
|
+
} else {
|
|
213
|
+
values.push(value);
|
|
214
|
+
}
|
|
215
|
+
if (updateOnDuplicate && createdAt && name === createdAt) continue;
|
|
216
|
+
updateOnDuplicateColumns.push(Model.unalias(name));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
const chunks = ['INSERT'];
|
|
222
|
+
|
|
223
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
224
|
+
const hintStr = this.formatOptimizerHints(spell);
|
|
225
|
+
if (hintStr) {
|
|
226
|
+
chunks.push(hintStr);
|
|
227
|
+
}
|
|
228
|
+
chunks.push(`INTO ${escapeId(Model.table)} (${columns.map(column => escapeId(column)).join(', ')})`);
|
|
229
|
+
if (placeholders.length) {
|
|
230
|
+
chunks.push(`VALUES ${placeholders.join(', ')}`);
|
|
231
|
+
} else {
|
|
232
|
+
chunks.push(`VALUES (${columns.map(_ => '?').join(', ')})`);
|
|
233
|
+
}
|
|
234
|
+
chunks.push(this.formatUpdateOnDuplicate(spell, updateOnDuplicateColumns));
|
|
235
|
+
chunks.push(this.formatReturning(spell));
|
|
236
|
+
return {
|
|
237
|
+
sql: chunks.join(' ').trim(),
|
|
238
|
+
values,
|
|
239
|
+
};
|
|
256
240
|
}
|
|
257
241
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
242
|
+
/**
|
|
243
|
+
* 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.
|
|
244
|
+
* @param {Spell} spell
|
|
245
|
+
*/
|
|
246
|
+
formatSelectWithoutJoin(spell) {
|
|
247
|
+
const { columns, whereConditions, groups, havingConditions, orders, rowCount, skip } = spell;
|
|
248
|
+
const chunks = ['SELECT'];
|
|
249
|
+
const values = [];
|
|
261
250
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
chunks.push(`HAVING ${formatConditions(spell, havingConditions)}`);
|
|
265
|
-
}
|
|
251
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
252
|
+
const hintStr = this.formatOptimizerHints(spell);
|
|
266
253
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (skip > 0) chunks.push(`OFFSET ${skip}`);
|
|
271
|
-
}
|
|
272
|
-
return { sql: chunks.join(' '), values };
|
|
273
|
-
}
|
|
254
|
+
if (hintStr) {
|
|
255
|
+
chunks.push(hintStr);
|
|
256
|
+
}
|
|
274
257
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
258
|
+
if (columns.length > 0) {
|
|
259
|
+
for (const column of columns) collectLiteral(spell, column, values);
|
|
260
|
+
const selects = [];
|
|
261
|
+
for (const token of columns) {
|
|
262
|
+
const column = formatExpr(spell, token);
|
|
263
|
+
if (!selects.includes(column)) selects.push(column);
|
|
264
|
+
}
|
|
265
|
+
chunks.push(`${selects.join(', ')}`);
|
|
266
|
+
} else {
|
|
267
|
+
chunks.push('*');
|
|
268
|
+
}
|
|
282
269
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
270
|
+
const table = formatExpr(spell, spell.table);
|
|
271
|
+
chunks.push(`FROM ${table}`);
|
|
272
|
+
if (spell.table.value instanceof spell.constructor) {
|
|
273
|
+
chunks.push(`AS t${spell.subqueryIndex++}`);
|
|
274
|
+
}
|
|
286
275
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
276
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
|
|
277
|
+
const indexHintStr = this.formatIndexHints(spell);
|
|
278
|
+
if (indexHintStr) {
|
|
279
|
+
chunks.push(indexHintStr);
|
|
280
|
+
}
|
|
290
281
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
282
|
+
if (whereConditions.length > 0) {
|
|
283
|
+
for (const condition of whereConditions) collectLiteral(spell, condition, values);
|
|
284
|
+
chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
|
|
285
|
+
}
|
|
295
286
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
function formatDelete(spell) {
|
|
301
|
-
const { Model, whereConditions } = spell;
|
|
302
|
-
const { shardingKey } = Model;
|
|
303
|
-
const { escapeId } = Model.driver;
|
|
304
|
-
const table = escapeId(Model.table);
|
|
287
|
+
if (groups.length > 0) {
|
|
288
|
+
const groupColumns = groups.map(group => formatExpr(spell, group));
|
|
289
|
+
chunks.push(`GROUP BY ${groupColumns.join(', ')}`);
|
|
290
|
+
}
|
|
305
291
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
292
|
+
if (havingConditions.length > 0) {
|
|
293
|
+
for (const condition of havingConditions) collectLiteral(spell, condition, values);
|
|
294
|
+
chunks.push(`HAVING ${formatConditions(spell, havingConditions)}`);
|
|
295
|
+
}
|
|
309
296
|
|
|
310
|
-
|
|
297
|
+
if (orders.length > 0) {
|
|
298
|
+
// ORDER BY FIND_IN_SET(`id`, '1,2,3')
|
|
299
|
+
for (const [ expr ] of orders) collectLiteral(spell, expr, values);
|
|
300
|
+
chunks.push(`ORDER BY ${this.formatOrders(spell, orders).join(', ')}`);
|
|
301
|
+
}
|
|
302
|
+
if (rowCount > 0) chunks.push(`LIMIT ${rowCount}`);
|
|
303
|
+
if (skip > 0) chunks.push(`OFFSET ${skip}`);
|
|
311
304
|
|
|
312
|
-
|
|
313
|
-
const hintStr = this.formatOptimizerHints(spell);
|
|
314
|
-
if (hintStr) {
|
|
315
|
-
chunks.push(hintStr);
|
|
305
|
+
return { sql: chunks.join(' '), values };
|
|
316
306
|
}
|
|
317
307
|
|
|
318
|
-
|
|
308
|
+
/**
|
|
309
|
+
* INSERT ... ON CONFLICT ... UPDATE SET
|
|
310
|
+
* - https://www.postgresql.org/docs/9.5/sql-insert.html
|
|
311
|
+
* - https://www.sqlite.org/lang_UPSERT.html
|
|
312
|
+
* @param {Spell} spell
|
|
313
|
+
*/
|
|
314
|
+
formatUpsert(spell) {
|
|
315
|
+
if (!spell.updateOnDuplicate) {
|
|
316
|
+
spell.updateOnDuplicate = true;
|
|
317
|
+
}
|
|
319
318
|
|
|
320
|
-
|
|
321
|
-
const values = [];
|
|
322
|
-
for (const condition of whereConditions) collectLiteral(spell, condition, values);
|
|
323
|
-
chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
|
|
319
|
+
let { sql, values } = this.formatInsert(spell);
|
|
324
320
|
return {
|
|
325
|
-
sql
|
|
326
|
-
values
|
|
321
|
+
sql,
|
|
322
|
+
values,
|
|
327
323
|
};
|
|
328
|
-
} else {
|
|
329
|
-
return { sql: chunks.join(' ') };
|
|
330
324
|
}
|
|
331
|
-
}
|
|
332
325
|
|
|
333
|
-
/**
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
326
|
+
/**
|
|
327
|
+
* @param {Spell} spell
|
|
328
|
+
* @returns returning sql string
|
|
329
|
+
*/
|
|
330
|
+
formatReturning(spell) {
|
|
331
|
+
const { Model, returning } = spell;
|
|
332
|
+
const { primaryColumn } = Model;
|
|
333
|
+
const { escapeId } = Model.driver;
|
|
334
|
+
|
|
335
|
+
let returnings;
|
|
336
|
+
if (returning === true) returnings = [ escapeId(primaryColumn) ];
|
|
337
|
+
if (Array.isArray(returning)) {
|
|
338
|
+
returnings = returning.map(escapeId);
|
|
339
|
+
}
|
|
340
|
+
return returnings && returnings.length? `RETURNING ${returnings.join(', ')}` : '';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {Spell} spell
|
|
345
|
+
* @param {Array} columns columns for value set
|
|
346
|
+
*/
|
|
347
|
+
formatUpdateOnDuplicate(spell, columns) {
|
|
348
|
+
const { updateOnDuplicate, uniqueKeys, Model } = spell;
|
|
349
|
+
if (!updateOnDuplicate) return '';
|
|
350
|
+
const { columnAttributes, primaryColumn } = Model;
|
|
351
|
+
const { escapeId } = Model.driver;
|
|
352
|
+
const actualUniqueKeys = [];
|
|
353
|
+
|
|
354
|
+
if (uniqueKeys) {
|
|
355
|
+
for (const field of [].concat(uniqueKeys)) {
|
|
356
|
+
actualUniqueKeys.push(escapeId(field));
|
|
356
357
|
}
|
|
357
358
|
} else {
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
// conflict_target must be unique
|
|
360
|
+
// get all unique keys
|
|
361
|
+
if (columnAttributes) {
|
|
362
|
+
for (const key in columnAttributes) {
|
|
363
|
+
const att = columnAttributes[key];
|
|
364
|
+
// use the first unique key
|
|
365
|
+
if (att.unique) {
|
|
366
|
+
actualUniqueKeys.push(escapeId(att.columnName));
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
360
370
|
}
|
|
371
|
+
if (!actualUniqueKeys.length) actualUniqueKeys.push(escapeId(primaryColumn));
|
|
372
|
+
// default use id as primary key
|
|
373
|
+
if (!actualUniqueKeys.length) actualUniqueKeys.push(escapeId('id'));
|
|
361
374
|
}
|
|
362
375
|
|
|
363
|
-
|
|
364
|
-
columns.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
updateOnDuplicateColumns.push(entry.columnName);
|
|
376
|
+
if (Array.isArray(updateOnDuplicate) && updateOnDuplicate.length) {
|
|
377
|
+
columns = updateOnDuplicate.map(column => (columnAttributes[column] && columnAttributes[column].columnName )|| column);
|
|
378
|
+
} else if (!columns.length) {
|
|
379
|
+
columns = Object.values(columnAttributes).map(({ columnName }) => columnName);
|
|
368
380
|
}
|
|
381
|
+
const updateKeys = columns.map((column) => `${escapeId(column)}=EXCLUDED.${escapeId(column)}`);
|
|
369
382
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
383
|
+
return `ON CONFLICT (${actualUniqueKeys.join(', ')}) DO UPDATE SET ${updateKeys.join(', ')}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Format a spell into UPDATE query
|
|
388
|
+
* @param {Spell} spell
|
|
389
|
+
*/
|
|
390
|
+
formatUpdate(spell) {
|
|
391
|
+
const { Model, sets, whereConditions } = spell;
|
|
392
|
+
const { shardingKey } = Model;
|
|
393
|
+
|
|
394
|
+
if (shardingKey) {
|
|
395
|
+
if (sets.hasOwnProperty(shardingKey) && sets[shardingKey] == null) {
|
|
396
|
+
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL`);
|
|
373
397
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
values.push(entry[name]);
|
|
398
|
+
if (!whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
|
|
399
|
+
throw new Error(`Sharding key ${Model.table}.${shardingKey} is required.`);
|
|
377
400
|
}
|
|
378
|
-
placeholders.push(`(${new Array(columnAttributes.length).fill('?').join(',')})`);
|
|
379
401
|
}
|
|
380
402
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL.`);
|
|
403
|
+
if (Object.keys(sets).length === 0) {
|
|
404
|
+
throw new Error('Unable to update with empty set');
|
|
384
405
|
}
|
|
406
|
+
|
|
407
|
+
const chunks = ['UPDATE'];
|
|
408
|
+
|
|
409
|
+
const values = [];
|
|
410
|
+
const assigns = [];
|
|
411
|
+
const { escapeId } = Model.driver;
|
|
385
412
|
for (const name in sets) {
|
|
386
413
|
const value = sets[name];
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
414
|
+
if (value && value.__expr) {
|
|
415
|
+
assigns.push(`${escapeId(Model.unalias(name))} = ${formatExpr(spell, value)}`);
|
|
416
|
+
collectLiteral(spell, value, values);
|
|
417
|
+
} else if (value instanceof Raw) {
|
|
418
|
+
assigns.push(`${escapeId(Model.unalias(name))} = ${value.value}`);
|
|
390
419
|
} else {
|
|
391
|
-
|
|
420
|
+
assigns.push(`${escapeId(Model.unalias(name))} = ?`);
|
|
421
|
+
values.push(sets[name]);
|
|
392
422
|
}
|
|
393
|
-
if (updateOnDuplicate && createdAt && name === createdAt) continue;
|
|
394
|
-
updateOnDuplicateColumns.push(Model.unalias(name));
|
|
395
423
|
}
|
|
396
|
-
}
|
|
397
424
|
|
|
425
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
426
|
+
const hintStr = this.formatOptimizerHints(spell);
|
|
427
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
|
|
428
|
+
const indexHintStr = this.formatIndexHints(spell);
|
|
398
429
|
|
|
399
|
-
|
|
430
|
+
if (hintStr) {
|
|
431
|
+
chunks.push(hintStr);
|
|
432
|
+
}
|
|
433
|
+
chunks.push(escapeId(Model.table));
|
|
434
|
+
if (indexHintStr) {
|
|
435
|
+
chunks.push(indexHintStr);
|
|
436
|
+
}
|
|
400
437
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
438
|
+
chunks.push(`SET ${assigns.join(', ')}`);
|
|
439
|
+
if (whereConditions.length > 0) {
|
|
440
|
+
for (const condition of whereConditions) collectLiteral(spell, condition, values);
|
|
441
|
+
chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
sql: chunks.join(' '),
|
|
446
|
+
values,
|
|
447
|
+
};
|
|
411
448
|
}
|
|
412
|
-
chunks.push(this.formatUpdateOnDuplicate(spell, updateOnDuplicateColumns));
|
|
413
|
-
chunks.push(this.formatReturning(spell));
|
|
414
|
-
return {
|
|
415
|
-
sql: chunks.join(' ').trim(),
|
|
416
|
-
values,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
449
|
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
450
|
+
/**
|
|
451
|
+
* Format the spell into a DELETE query.
|
|
452
|
+
* @param {Spell} spell
|
|
453
|
+
*/
|
|
454
|
+
formatDelete(spell) {
|
|
455
|
+
const { Model, whereConditions } = spell;
|
|
456
|
+
const { shardingKey } = Model;
|
|
457
|
+
const { escapeId } = Model.driver;
|
|
458
|
+
const table = escapeId(Model.table);
|
|
427
459
|
|
|
428
|
-
|
|
429
|
-
if (sets.hasOwnProperty(shardingKey) && sets[shardingKey] == null) {
|
|
430
|
-
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL`);
|
|
431
|
-
}
|
|
432
|
-
if (!whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
|
|
460
|
+
if (shardingKey && !whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
|
|
433
461
|
throw new Error(`Sharding key ${Model.table}.${shardingKey} is required.`);
|
|
434
462
|
}
|
|
435
|
-
}
|
|
436
463
|
|
|
437
|
-
|
|
438
|
-
throw new Error('Unable to update with empty set');
|
|
439
|
-
}
|
|
464
|
+
const chunks = ['DELETE'];
|
|
440
465
|
|
|
441
|
-
|
|
466
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
467
|
+
const hintStr = this.formatOptimizerHints(spell);
|
|
468
|
+
if (hintStr) {
|
|
469
|
+
chunks.push(hintStr);
|
|
470
|
+
}
|
|
442
471
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
472
|
+
chunks.push(`FROM ${table}`);
|
|
473
|
+
|
|
474
|
+
if (whereConditions.length > 0) {
|
|
475
|
+
const values = [];
|
|
476
|
+
for (const condition of whereConditions) collectLiteral(spell, condition, values);
|
|
477
|
+
chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
|
|
478
|
+
return {
|
|
479
|
+
sql: chunks.join(' '),
|
|
480
|
+
values
|
|
481
|
+
};
|
|
453
482
|
} else {
|
|
454
|
-
|
|
455
|
-
values.push(sets[name]);
|
|
483
|
+
return { sql: chunks.join(' ') };
|
|
456
484
|
}
|
|
457
485
|
}
|
|
458
486
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
487
|
+
/**
|
|
488
|
+
* To help choosing the right function when formatting a spell into SELECT query.
|
|
489
|
+
* @param {Spell} spell
|
|
490
|
+
*/
|
|
491
|
+
formatSelect(spell) {
|
|
492
|
+
const { whereConditions } = spell;
|
|
493
|
+
const { shardingKey, table } = spell.Model;
|
|
463
494
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
495
|
+
if (shardingKey && !whereConditions.some(condition => findExpr(condition, { type: 'id', value: shardingKey }))) {
|
|
496
|
+
throw new Error(`Sharding key ${table}.${shardingKey} is required.`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (spell.skip > 0 && spell.rowCount == null) {
|
|
500
|
+
throw new Error('Unable to query with OFFSET yet without LIMIT');
|
|
501
|
+
}
|
|
471
502
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
|
|
503
|
+
return Object.keys(spell.joins).length > 0
|
|
504
|
+
? this.formatSelectWithJoin(spell)
|
|
505
|
+
: this.formatSelectWithoutJoin(spell);
|
|
476
506
|
}
|
|
477
507
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Format a spell with joins into a full SELECT query.
|
|
510
|
+
* @param {Spell} spell
|
|
511
|
+
*/
|
|
512
|
+
formatSelectWithJoin(spell) {
|
|
513
|
+
// Since it is a JOIN query, make sure columns are always qualified.
|
|
514
|
+
qualify(spell);
|
|
483
515
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
// use the first unique key
|
|
506
|
-
if (att.unique) {
|
|
507
|
-
actualUniqueKeys.push(escapeId(att.columnName));
|
|
508
|
-
break;
|
|
516
|
+
const { Model, whereConditions, groups, havingConditions, orders, rowCount, skip, joins } = spell;
|
|
517
|
+
const { escapeId } = Model.driver;
|
|
518
|
+
const baseName = Model.tableAlias;
|
|
519
|
+
|
|
520
|
+
const chunks = ['SELECT'];
|
|
521
|
+
const values = [];
|
|
522
|
+
const selects = formatSelectExpr(spell, values);
|
|
523
|
+
|
|
524
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
|
|
525
|
+
const hintStr = this.formatOptimizerHints(spell);
|
|
526
|
+
|
|
527
|
+
if (hintStr) {
|
|
528
|
+
chunks.push(hintStr);
|
|
529
|
+
}
|
|
530
|
+
chunks.push(selects.join(', '));
|
|
531
|
+
|
|
532
|
+
let hoistable = skip > 0 || rowCount > 0;
|
|
533
|
+
if (hoistable) {
|
|
534
|
+
function checkQualifier({ type, qualifiers = [] }) {
|
|
535
|
+
if (type === 'id' && qualifiers.length> 0 && !qualifiers.includes(baseName)) {
|
|
536
|
+
hoistable = false;
|
|
509
537
|
}
|
|
510
538
|
}
|
|
539
|
+
for (const condition of whereConditions) walkExpr(condition, checkQualifier);
|
|
540
|
+
for (const orderExpr of orders) walkExpr(orderExpr[0], checkQualifier);
|
|
511
541
|
}
|
|
512
|
-
if (!actualUniqueKeys.length) actualUniqueKeys.push(escapeId(primaryColumn));
|
|
513
|
-
// default use id as primary key
|
|
514
|
-
if (!actualUniqueKeys.length) actualUniqueKeys.push(escapeId('id'));
|
|
515
|
-
}
|
|
516
542
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
543
|
+
if (hoistable) {
|
|
544
|
+
const subspell = createSubspell(spell);
|
|
545
|
+
const subquery = this.formatSelectWithoutJoin(subspell);
|
|
546
|
+
values.push(...subquery.values);
|
|
547
|
+
chunks.push(`FROM (${subquery.sql}) AS ${escapeId(baseName)}`);
|
|
548
|
+
} else {
|
|
549
|
+
chunks.push(`FROM ${escapeId(Model.table)} AS ${escapeId(baseName)}`);
|
|
550
|
+
}
|
|
526
551
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const { Model, returning } = spell;
|
|
533
|
-
const { primaryColumn } = Model;
|
|
534
|
-
const { escapeId } = Model.driver;
|
|
552
|
+
for (const qualifier in joins) {
|
|
553
|
+
const { Model: RefModel, on } = joins[qualifier];
|
|
554
|
+
collectLiteral(spell, on, values);
|
|
555
|
+
chunks.push(`LEFT JOIN ${escapeId(RefModel.table)} AS ${escapeId(qualifier)} ON ${formatExpr(spell, on)}`);
|
|
556
|
+
}
|
|
535
557
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
return returnings && returnings.length? `RETURNING ${returnings.join(', ')}` : '';
|
|
542
|
-
}
|
|
558
|
+
// see https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
|
|
559
|
+
const indexHintStr = this.formatIndexHints(spell);
|
|
560
|
+
if (indexHintStr) {
|
|
561
|
+
chunks.push(indexHintStr);
|
|
562
|
+
}
|
|
543
563
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
* @param {Spell} spell
|
|
549
|
-
*/
|
|
550
|
-
function formatUpsert(spell) {
|
|
551
|
-
if (!spell.updateOnDuplicate) {
|
|
552
|
-
spell.updateOnDuplicate = true;
|
|
553
|
-
}
|
|
564
|
+
if (whereConditions.length > 0) {
|
|
565
|
+
for (const condition of whereConditions) collectLiteral(spell, condition, values);
|
|
566
|
+
chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
|
|
567
|
+
}
|
|
554
568
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
values,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
569
|
+
if (groups.length > 0) {
|
|
570
|
+
chunks.push(`GROUP BY ${groups.map(group => formatExpr(spell, group)).join(', ')}`);
|
|
571
|
+
}
|
|
561
572
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
573
|
+
if (havingConditions.length > 0) {
|
|
574
|
+
for (const condition of havingConditions) collectLiteral(spell, condition, values);
|
|
575
|
+
chunks.push(`HAVING ${formatConditions(spell, havingConditions)}`);
|
|
576
|
+
}
|
|
565
577
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
case 'select':
|
|
571
|
-
return this.formatSelect(spell);
|
|
572
|
-
case 'update':
|
|
573
|
-
return this.formatUpdate(spell);
|
|
574
|
-
case 'delete':
|
|
575
|
-
return this.formatDelete(spell);
|
|
576
|
-
case 'upsert':
|
|
577
|
-
return this.formatUpsert(spell);
|
|
578
|
-
default:
|
|
579
|
-
throw new Error(`Unsupported SQL command ${spell.command}`);
|
|
578
|
+
if (orders.length > 0) chunks.push(`ORDER BY ${this.formatOrders(spell, orders).join(', ')}`);
|
|
579
|
+
if (!hoistable) {
|
|
580
|
+
if (rowCount > 0) chunks.push(`LIMIT ${rowCount}`);
|
|
581
|
+
if (skip > 0) chunks.push(`OFFSET ${skip}`);
|
|
580
582
|
}
|
|
581
|
-
|
|
583
|
+
return { sql: chunks.join(' '), values };
|
|
584
|
+
}
|
|
582
585
|
|
|
583
586
|
/**
|
|
584
|
-
*
|
|
585
|
-
* @
|
|
587
|
+
* Format orders into ORDER BY clause in SQL
|
|
588
|
+
* @param {Spell} spell
|
|
589
|
+
* @param {Object[]} orders
|
|
586
590
|
*/
|
|
587
|
-
|
|
588
|
-
return
|
|
589
|
-
|
|
591
|
+
formatOrders(spell, orders) {
|
|
592
|
+
return orders.map(([token, order]) => {
|
|
593
|
+
const column = formatExpr(spell, token);
|
|
594
|
+
return order == 'desc' ? `${column} DESC` : column;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
590
597
|
|
|
591
|
-
/**
|
|
592
|
-
* @abstract
|
|
593
|
-
* @returns {string} index hints
|
|
594
|
-
*/
|
|
595
|
-
formatIndexHints() {
|
|
596
|
-
return '';
|
|
597
|
-
},
|
|
598
|
-
|
|
599
|
-
formatInsert,
|
|
600
|
-
formatSelect,
|
|
601
|
-
formatUpdate,
|
|
602
|
-
formatDelete,
|
|
603
|
-
formatUpsert,
|
|
604
|
-
formatSelectWithJoin,
|
|
605
|
-
formatSelectWithoutJoin,
|
|
606
|
-
formatUpdateOnDuplicate,
|
|
607
|
-
formatReturning,
|
|
608
|
-
formatOrders
|
|
609
598
|
};
|
|
599
|
+
|
|
600
|
+
module.exports = SpellBook;
|