befly 3.20.11 → 3.21.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/lib/dbHelper.js +1626 -0
- package/lib/dbUtil.js +988 -0
- package/lib/sqlBuilder.js +714 -0
- package/package.json +2 -2
- package/plugins/mysql.js +1 -1
- package/scripts/syncDb/context.js +1 -1
- package/lib/dbHelper/builders.js +0 -658
- package/lib/dbHelper/dataOps.js +0 -578
- package/lib/dbHelper/execute.js +0 -128
- package/lib/dbHelper/index.js +0 -24
- package/lib/dbHelper/util.js +0 -95
- package/lib/dbHelper/validate.js +0 -695
- package/lib/sqlBuilder/batch.js +0 -114
- package/lib/sqlBuilder/check.js +0 -82
- package/lib/sqlBuilder/compiler.js +0 -360
- package/lib/sqlBuilder/index.js +0 -200
- package/lib/sqlBuilder/parser.js +0 -305
- package/lib/sqlBuilder/util.js +0 -273
package/lib/dbHelper/dataOps.js
DELETED
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
import { isNumber } from "../../utils/is.js";
|
|
2
|
-
import { snakeCase } from "../../utils/util.js";
|
|
3
|
-
import { Logger } from "../logger.js";
|
|
4
|
-
import { SqlBuilder } from "../sqlBuilder/index.js";
|
|
5
|
-
import { normalizeSqlMetaNumber, quoteIdentMySql } from "./util.js";
|
|
6
|
-
import { addDefaultStateFilter, buildInsertRow, buildPartialUpdateData, buildUpdateRow, clearDeep, whereKeysToSnake } from "./builders.js";
|
|
7
|
-
import { assertBatchInsertRowsConsistent, assertNoUndefinedInRecord, validateGeneratedBatchId, validateIncrementOptions, validateInsertBatchSize, validateNoLeftJoinReadOptions, validatePageLimitRange, validateSafeFieldName, validateTableBatchDataOptions, validateTableDataOptions, validateTableName, validateTableWhereOptions } from "./validate.js";
|
|
8
|
-
|
|
9
|
-
export const dataOpsMethods = {
|
|
10
|
-
prepareSingleTableWhere(table, where, useDefaultStateFilter = true) {
|
|
11
|
-
const snakeTable = snakeCase(table);
|
|
12
|
-
const snakeWhere = whereKeysToSnake(clearDeep(where || {}));
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
snakeTable: snakeTable,
|
|
16
|
-
whereFiltered: useDefaultStateFilter ? addDefaultStateFilter(snakeWhere, snakeTable, false, this.beflyMode) : snakeWhere
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
|
|
20
|
-
async createInsertRows(table, snakeTable, dataList, now) {
|
|
21
|
-
if (this.beflyMode === "manual") {
|
|
22
|
-
return {
|
|
23
|
-
ids: [],
|
|
24
|
-
processedList: dataList.map((data) =>
|
|
25
|
-
buildInsertRow({
|
|
26
|
-
data: data,
|
|
27
|
-
now: now,
|
|
28
|
-
beflyMode: this.beflyMode
|
|
29
|
-
})
|
|
30
|
-
)
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const ids = [];
|
|
35
|
-
try {
|
|
36
|
-
for (let i = 0; i < dataList.length; i++) {
|
|
37
|
-
ids.push(await this.redis.genTimeID());
|
|
38
|
-
}
|
|
39
|
-
} catch (error) {
|
|
40
|
-
if (dataList.length === 1) {
|
|
41
|
-
throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, {
|
|
42
|
-
cause: error,
|
|
43
|
-
code: "runtime",
|
|
44
|
-
subsystem: "db",
|
|
45
|
-
operation: "genTimeId",
|
|
46
|
-
table: table
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const processedList = dataList.map((data, index) => {
|
|
53
|
-
const id = ids[index];
|
|
54
|
-
if (dataList.length > 1) {
|
|
55
|
-
validateGeneratedBatchId(id, snakeTable, index);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return buildInsertRow({
|
|
59
|
-
data: data,
|
|
60
|
-
id: id,
|
|
61
|
-
now: now,
|
|
62
|
-
beflyMode: this.beflyMode
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
ids: ids,
|
|
68
|
-
processedList: processedList
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
resolveInsertedData(table, count, executeRes, generatedIds, operation) {
|
|
73
|
-
const lastInsertRowidNum = normalizeSqlMetaNumber(executeRes.data?.lastInsertRowid);
|
|
74
|
-
|
|
75
|
-
if (this.beflyMode === "manual") {
|
|
76
|
-
if (lastInsertRowidNum <= 0) {
|
|
77
|
-
throw new Error(operation === "insBatch" ? `批量插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})` : `插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})`, {
|
|
78
|
-
cause: null,
|
|
79
|
-
code: "runtime"
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (count === 1) {
|
|
84
|
-
return lastInsertRowidNum;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const outIds = [];
|
|
88
|
-
for (let i = 0; i < count; i++) {
|
|
89
|
-
outIds.push(lastInsertRowidNum + i);
|
|
90
|
-
}
|
|
91
|
-
return outIds;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (count === 1) {
|
|
95
|
-
const generatedId = generatedIds[0];
|
|
96
|
-
return (isNumber(generatedId) ? generatedId : 0) || lastInsertRowidNum || 0;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return generatedIds;
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
// 读取操作
|
|
103
|
-
async getCount(options) {
|
|
104
|
-
const { prepared, whereFiltered } = await this.prepareReadContext(options, "getCount.options");
|
|
105
|
-
const result = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as count");
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
data: result.total,
|
|
109
|
-
sql: result.sql
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
async getOne(options) {
|
|
114
|
-
const { prepared, whereFiltered } = await this.prepareReadContext(options, "getOne.options");
|
|
115
|
-
const builder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered);
|
|
116
|
-
this.applyLeftJoins(builder, prepared.leftJoins);
|
|
117
|
-
|
|
118
|
-
const { sql, params } = builder.toSelectSql();
|
|
119
|
-
const executeRes = await this.execute(sql, params);
|
|
120
|
-
const result = executeRes.data;
|
|
121
|
-
|
|
122
|
-
const data = this.normalizeRowData(result?.[0] || null);
|
|
123
|
-
return {
|
|
124
|
-
data: data,
|
|
125
|
-
sql: executeRes.sql
|
|
126
|
-
};
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
async getDetail(options) {
|
|
130
|
-
return await this.getOne(options);
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
async getList(options) {
|
|
134
|
-
const { prepared, whereFiltered } = await this.prepareReadContext(options, "getList.options");
|
|
135
|
-
validatePageLimitRange(prepared, options.table);
|
|
136
|
-
const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
|
|
137
|
-
const total = countResult.total;
|
|
138
|
-
|
|
139
|
-
const offset = (prepared.page - 1) * prepared.limit;
|
|
140
|
-
const dataBuilder = this.createListBuilder(prepared, whereFiltered, prepared.limit, offset);
|
|
141
|
-
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
142
|
-
|
|
143
|
-
if (total === 0) {
|
|
144
|
-
return {
|
|
145
|
-
data: {
|
|
146
|
-
lists: [],
|
|
147
|
-
total: 0,
|
|
148
|
-
page: prepared.page,
|
|
149
|
-
limit: prepared.limit,
|
|
150
|
-
pages: 0
|
|
151
|
-
},
|
|
152
|
-
sql: {
|
|
153
|
-
count: countResult.sql,
|
|
154
|
-
data: {
|
|
155
|
-
sql: dataSql,
|
|
156
|
-
params: dataParams,
|
|
157
|
-
duration: 0
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
const listExecuteRes = await this.execute(dataSql, dataParams);
|
|
163
|
-
const list = listExecuteRes.data || [];
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
data: {
|
|
167
|
-
lists: this.normalizeListData(list),
|
|
168
|
-
total: total,
|
|
169
|
-
page: prepared.page,
|
|
170
|
-
limit: prepared.limit,
|
|
171
|
-
pages: Math.ceil(total / prepared.limit)
|
|
172
|
-
},
|
|
173
|
-
sql: {
|
|
174
|
-
count: countResult.sql,
|
|
175
|
-
data: listExecuteRes.sql
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
async getAll(options) {
|
|
181
|
-
const MAX_LIMIT = 100000;
|
|
182
|
-
const WARNING_LIMIT = 10000;
|
|
183
|
-
const prepareOptions = this.buildQueryOptions(options, { page: 1, limit: 10 });
|
|
184
|
-
const { prepared, whereFiltered } = await this.prepareReadContext(prepareOptions, "getAll.options");
|
|
185
|
-
const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
|
|
186
|
-
const total = countResult.total;
|
|
187
|
-
|
|
188
|
-
if (total === 0) {
|
|
189
|
-
return {
|
|
190
|
-
data: {
|
|
191
|
-
lists: [],
|
|
192
|
-
total: 0
|
|
193
|
-
},
|
|
194
|
-
sql: {
|
|
195
|
-
count: countResult.sql
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const dataBuilder = this.createListBuilder(prepared, whereFiltered, MAX_LIMIT, null);
|
|
201
|
-
|
|
202
|
-
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
203
|
-
const listExecuteRes = await this.execute(dataSql, dataParams);
|
|
204
|
-
const result = listExecuteRes.data || [];
|
|
205
|
-
|
|
206
|
-
if (result.length >= WARNING_LIMIT) {
|
|
207
|
-
Logger.warn("getAll 返回数据过多,建议使用 getList 分页查询", { table: options.table, count: result.length, total: total });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (result.length >= MAX_LIMIT) {
|
|
211
|
-
Logger.warn(`getAll 达到最大限制 ${MAX_LIMIT},实际总数 ${total},只返回前 ${MAX_LIMIT} 条`, { table: options.table, limit: MAX_LIMIT, total: total });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const lists = this.normalizeListData(result);
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
data: {
|
|
218
|
-
lists: lists,
|
|
219
|
-
total: total
|
|
220
|
-
},
|
|
221
|
-
sql: {
|
|
222
|
-
count: countResult.sql,
|
|
223
|
-
data: listExecuteRes.sql
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
},
|
|
227
|
-
|
|
228
|
-
async exists(options) {
|
|
229
|
-
validateNoLeftJoinReadOptions(options, "exists", "exists 不支持 leftJoin(请使用显式 query 或拆分查询)");
|
|
230
|
-
const prepared = this.prepareSingleTableWhere(options.table, options.where, true);
|
|
231
|
-
|
|
232
|
-
const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(prepared.snakeTable).where(prepared.whereFiltered).limit(1);
|
|
233
|
-
const { sql, params } = builder.toSelectSql();
|
|
234
|
-
const executeRes = await this.execute(sql, params);
|
|
235
|
-
const exists = (executeRes.data?.[0]?.cnt || 0) > 0;
|
|
236
|
-
return { data: exists, sql: executeRes.sql };
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
async getFieldValue(options) {
|
|
240
|
-
validateNoLeftJoinReadOptions(options, "getFieldValue", "getFieldValue 不支持 leftJoin(请使用 getOne/getList 并自行取字段)");
|
|
241
|
-
const field = options.field;
|
|
242
|
-
validateSafeFieldName(field);
|
|
243
|
-
|
|
244
|
-
const oneOptions = {
|
|
245
|
-
table: options.table
|
|
246
|
-
};
|
|
247
|
-
if (options.where !== undefined) oneOptions.where = options.where;
|
|
248
|
-
if (options.leftJoin !== undefined) oneOptions.leftJoin = options.leftJoin;
|
|
249
|
-
if (options.orderBy !== undefined) oneOptions.orderBy = options.orderBy;
|
|
250
|
-
if (options.page !== undefined) oneOptions.page = options.page;
|
|
251
|
-
if (options.limit !== undefined) oneOptions.limit = options.limit;
|
|
252
|
-
oneOptions.fields = [field];
|
|
253
|
-
|
|
254
|
-
const oneRes = await this.getOne(oneOptions);
|
|
255
|
-
|
|
256
|
-
const result = oneRes.data;
|
|
257
|
-
const value = this.resolveFieldValue(result, field);
|
|
258
|
-
return {
|
|
259
|
-
data: value,
|
|
260
|
-
sql: oneRes.sql
|
|
261
|
-
};
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
// 写入操作
|
|
265
|
-
|
|
266
|
-
async insData(options) {
|
|
267
|
-
validateTableDataOptions(options, "insData");
|
|
268
|
-
const { table, data } = options;
|
|
269
|
-
const snakeTable = snakeCase(table);
|
|
270
|
-
const now = Date.now();
|
|
271
|
-
const insertRows = await this.createInsertRows(table, snakeTable, [data], now);
|
|
272
|
-
const processed = insertRows.processedList[0];
|
|
273
|
-
|
|
274
|
-
assertNoUndefinedInRecord(processed, `insData 插入数据 (table: ${snakeTable})`);
|
|
275
|
-
|
|
276
|
-
const builder = this.createSqlBuilder();
|
|
277
|
-
const { sql, params } = builder.toInsertSql(snakeTable, processed);
|
|
278
|
-
const executeRes = await this.execute(sql, params);
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
data: this.resolveInsertedData(table, 1, executeRes, insertRows.ids, "insData"),
|
|
282
|
-
sql: executeRes.sql
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
async insBatch(table, dataList) {
|
|
287
|
-
validateTableBatchDataOptions(table, dataList, "insBatch");
|
|
288
|
-
if (dataList.length === 0) {
|
|
289
|
-
return {
|
|
290
|
-
data: [],
|
|
291
|
-
sql: { sql: "", params: [], duration: 0 }
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const MAX_BATCH_SIZE = 1000;
|
|
296
|
-
validateInsertBatchSize(dataList.length, MAX_BATCH_SIZE);
|
|
297
|
-
|
|
298
|
-
const snakeTable = snakeCase(table);
|
|
299
|
-
const now = Date.now();
|
|
300
|
-
const insertRows = await this.createInsertRows(table, snakeTable, dataList, now);
|
|
301
|
-
const processedList = insertRows.processedList;
|
|
302
|
-
|
|
303
|
-
const insertFields = assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
|
|
304
|
-
const builder = this.createSqlBuilder();
|
|
305
|
-
const { sql, params } = builder.toInsertSql(snakeTable, processedList);
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
const executeRes = await this.execute(sql, params);
|
|
309
|
-
return {
|
|
310
|
-
data: this.resolveInsertedData(table, dataList.length, executeRes, insertRows.ids, "insBatch"),
|
|
311
|
-
sql: executeRes.sql
|
|
312
|
-
};
|
|
313
|
-
} catch (error) {
|
|
314
|
-
Logger.error("批量插入失败", error, {
|
|
315
|
-
table: table,
|
|
316
|
-
snakeTable: snakeTable,
|
|
317
|
-
count: dataList.length,
|
|
318
|
-
fields: insertFields
|
|
319
|
-
});
|
|
320
|
-
throw new Error("批量插入失败", {
|
|
321
|
-
cause: error,
|
|
322
|
-
code: "runtime",
|
|
323
|
-
subsystem: "sql",
|
|
324
|
-
operation: "insBatch",
|
|
325
|
-
table: table,
|
|
326
|
-
snakeTable: snakeTable,
|
|
327
|
-
count: dataList.length,
|
|
328
|
-
fields: insertFields
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
|
|
333
|
-
async delForceBatch(table, ids) {
|
|
334
|
-
validateTableName(table, "delForceBatch.table");
|
|
335
|
-
if (ids.length === 0) {
|
|
336
|
-
return {
|
|
337
|
-
data: 0,
|
|
338
|
-
sql: { sql: "", params: [], duration: 0 }
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const snakeTable = snakeCase(table);
|
|
343
|
-
|
|
344
|
-
const query = SqlBuilder.toDeleteInSql({
|
|
345
|
-
table: snakeTable,
|
|
346
|
-
idField: "id",
|
|
347
|
-
ids: ids,
|
|
348
|
-
quoteIdent: quoteIdentMySql
|
|
349
|
-
});
|
|
350
|
-
const executeRes = await this.execute(query.sql, query.params);
|
|
351
|
-
const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
|
|
352
|
-
return {
|
|
353
|
-
data: changes,
|
|
354
|
-
sql: executeRes.sql
|
|
355
|
-
};
|
|
356
|
-
},
|
|
357
|
-
|
|
358
|
-
async updBatch(table, dataList) {
|
|
359
|
-
validateTableBatchDataOptions(table, dataList, "updBatch");
|
|
360
|
-
if (dataList.length === 0) {
|
|
361
|
-
return {
|
|
362
|
-
data: 0,
|
|
363
|
-
sql: { sql: "", params: [], duration: 0 }
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const snakeTable = snakeCase(table);
|
|
368
|
-
const now = Date.now();
|
|
369
|
-
|
|
370
|
-
const processedList = [];
|
|
371
|
-
const fieldSet = new Set();
|
|
372
|
-
|
|
373
|
-
for (const item of dataList) {
|
|
374
|
-
const userData = buildPartialUpdateData({ data: item.data, allowState: true });
|
|
375
|
-
|
|
376
|
-
for (const key of Object.keys(userData)) {
|
|
377
|
-
fieldSet.add(key);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
processedList.push({ id: item.id, data: userData });
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const fields = Array.from(fieldSet).sort();
|
|
384
|
-
if (fields.length === 0) {
|
|
385
|
-
return {
|
|
386
|
-
data: 0,
|
|
387
|
-
sql: { sql: "", params: [], duration: 0 }
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const query = SqlBuilder.toUpdateCaseByIdSql({
|
|
392
|
-
table: snakeTable,
|
|
393
|
-
idField: "id",
|
|
394
|
-
rows: processedList,
|
|
395
|
-
fields: fields,
|
|
396
|
-
quoteIdent: quoteIdentMySql,
|
|
397
|
-
updatedAtField: this.beflyMode === "auto" ? "updated_at" : "",
|
|
398
|
-
updatedAtValue: this.beflyMode === "auto" ? now : null,
|
|
399
|
-
stateField: "state",
|
|
400
|
-
stateGtZero: this.beflyMode === "auto"
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
const executeRes = await this.execute(query.sql, query.params);
|
|
404
|
-
const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
|
|
405
|
-
return {
|
|
406
|
-
data: changes,
|
|
407
|
-
sql: executeRes.sql
|
|
408
|
-
};
|
|
409
|
-
},
|
|
410
|
-
|
|
411
|
-
async updData(options) {
|
|
412
|
-
validateTableDataOptions(options, "updData");
|
|
413
|
-
validateTableWhereOptions(options, "updData", true);
|
|
414
|
-
const { table, data, where } = options;
|
|
415
|
-
const prepared = this.prepareSingleTableWhere(table, where, true);
|
|
416
|
-
|
|
417
|
-
const processed = buildUpdateRow({ data: data, now: Date.now(), allowState: true, beflyMode: this.beflyMode });
|
|
418
|
-
const builder = this.createSqlBuilder().where(prepared.whereFiltered);
|
|
419
|
-
const { sql, params } = builder.toUpdateSql(prepared.snakeTable, processed);
|
|
420
|
-
|
|
421
|
-
const executeRes = await this.execute(sql, params);
|
|
422
|
-
const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
|
|
423
|
-
return {
|
|
424
|
-
data: changes,
|
|
425
|
-
sql: executeRes.sql
|
|
426
|
-
};
|
|
427
|
-
},
|
|
428
|
-
|
|
429
|
-
async delData(options) {
|
|
430
|
-
validateTableWhereOptions(options, "delData", true);
|
|
431
|
-
const { table, where } = options;
|
|
432
|
-
const prepared = this.prepareSingleTableWhere(table, where, true);
|
|
433
|
-
const now = Date.now();
|
|
434
|
-
const processed = {
|
|
435
|
-
state: 0,
|
|
436
|
-
deleted_at: now
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
if (this.beflyMode === "auto") {
|
|
440
|
-
processed.updated_at = now;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const builder = this.createSqlBuilder().where(prepared.whereFiltered);
|
|
444
|
-
const { sql, params } = builder.toUpdateSql(prepared.snakeTable, processed);
|
|
445
|
-
const executeRes = await this.execute(sql, params);
|
|
446
|
-
const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
data: changes,
|
|
450
|
-
sql: executeRes.sql
|
|
451
|
-
};
|
|
452
|
-
},
|
|
453
|
-
|
|
454
|
-
async delForce(options) {
|
|
455
|
-
validateTableWhereOptions(options, "delForce", true);
|
|
456
|
-
const { table, where } = options;
|
|
457
|
-
|
|
458
|
-
const prepared = this.prepareSingleTableWhere(table, where, false);
|
|
459
|
-
|
|
460
|
-
const builder = this.createSqlBuilder().where(prepared.whereFiltered);
|
|
461
|
-
const { sql, params } = builder.toDeleteSql(prepared.snakeTable);
|
|
462
|
-
|
|
463
|
-
const executeRes = await this.execute(sql, params);
|
|
464
|
-
const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
|
|
465
|
-
return {
|
|
466
|
-
data: changes,
|
|
467
|
-
sql: executeRes.sql
|
|
468
|
-
};
|
|
469
|
-
},
|
|
470
|
-
|
|
471
|
-
async disableData(options) {
|
|
472
|
-
validateTableWhereOptions(options, "disableData", true);
|
|
473
|
-
const { table, where } = options;
|
|
474
|
-
|
|
475
|
-
return await this.updData({
|
|
476
|
-
table: table,
|
|
477
|
-
data: {
|
|
478
|
-
state: 2
|
|
479
|
-
},
|
|
480
|
-
where: where
|
|
481
|
-
});
|
|
482
|
-
},
|
|
483
|
-
|
|
484
|
-
async enableData(options) {
|
|
485
|
-
validateTableWhereOptions(options, "enableData", true);
|
|
486
|
-
const { table, where } = options;
|
|
487
|
-
|
|
488
|
-
return await this.updData({
|
|
489
|
-
table: table,
|
|
490
|
-
data: {
|
|
491
|
-
state: 1
|
|
492
|
-
},
|
|
493
|
-
where: where
|
|
494
|
-
});
|
|
495
|
-
},
|
|
496
|
-
|
|
497
|
-
async increment(table, field, where, value = 1) {
|
|
498
|
-
validateIncrementOptions(table, field, where, value, "increment");
|
|
499
|
-
const prepared = this.prepareSingleTableWhere(table, where, true);
|
|
500
|
-
const snakeField = snakeCase(field);
|
|
501
|
-
|
|
502
|
-
const builder = this.createSqlBuilder().where(prepared.whereFiltered);
|
|
503
|
-
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
504
|
-
|
|
505
|
-
const quotedTable = quoteIdentMySql(prepared.snakeTable);
|
|
506
|
-
const quotedField = quoteIdentMySql(snakeField);
|
|
507
|
-
const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
|
|
508
|
-
|
|
509
|
-
const params = [value];
|
|
510
|
-
for (const param of whereParams) {
|
|
511
|
-
params.push(param);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const executeRes = await this.execute(sql, params);
|
|
515
|
-
const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
|
|
516
|
-
return {
|
|
517
|
-
data: changes,
|
|
518
|
-
sql: executeRes.sql
|
|
519
|
-
};
|
|
520
|
-
},
|
|
521
|
-
|
|
522
|
-
async decrement(table, field, where, value = 1) {
|
|
523
|
-
return await this.increment(table, field, where, -value);
|
|
524
|
-
},
|
|
525
|
-
|
|
526
|
-
async trans(callback) {
|
|
527
|
-
const abortMark = "__beflyTransAbort";
|
|
528
|
-
|
|
529
|
-
if (this.isTransaction) {
|
|
530
|
-
const result = await callback(this);
|
|
531
|
-
if (result?.code !== undefined && result.code !== 0) {
|
|
532
|
-
const abortError = new Error("TRANSACTION_ABORT", {
|
|
533
|
-
cause: null,
|
|
534
|
-
code: "runtime"
|
|
535
|
-
});
|
|
536
|
-
abortError[abortMark] = true;
|
|
537
|
-
abortError.payload = result;
|
|
538
|
-
throw abortError;
|
|
539
|
-
}
|
|
540
|
-
return result;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (typeof this.sql?.begin !== "function") {
|
|
544
|
-
throw new Error("不支持事务 begin() 方法", {
|
|
545
|
-
cause: null,
|
|
546
|
-
code: "runtime"
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
return await this.sql.begin(async (tx) => {
|
|
552
|
-
const trans = new this.constructor({
|
|
553
|
-
redis: this.redis,
|
|
554
|
-
dbName: this.dbName,
|
|
555
|
-
sql: tx,
|
|
556
|
-
isTransaction: true,
|
|
557
|
-
beflyMode: this.beflyMode
|
|
558
|
-
});
|
|
559
|
-
const result = await callback(trans);
|
|
560
|
-
if (result?.code !== undefined && result.code !== 0) {
|
|
561
|
-
const abortError = new Error("TRANSACTION_ABORT", {
|
|
562
|
-
cause: null,
|
|
563
|
-
code: "runtime"
|
|
564
|
-
});
|
|
565
|
-
abortError[abortMark] = true;
|
|
566
|
-
abortError.payload = result;
|
|
567
|
-
throw abortError;
|
|
568
|
-
}
|
|
569
|
-
return result;
|
|
570
|
-
});
|
|
571
|
-
} catch (error) {
|
|
572
|
-
if (error && error[abortMark] === true) {
|
|
573
|
-
return error.payload;
|
|
574
|
-
}
|
|
575
|
-
throw error;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
};
|
package/lib/dbHelper/execute.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { toSqlParams } from "../sqlBuilder/util.js";
|
|
2
|
-
import { snakeCase } from "../../utils/util.js";
|
|
3
|
-
import { validateExecuteSql } from "./validate.js";
|
|
4
|
-
|
|
5
|
-
function safeStringify(value) {
|
|
6
|
-
try {
|
|
7
|
-
return JSON.stringify(value);
|
|
8
|
-
} catch {
|
|
9
|
-
try {
|
|
10
|
-
return String(value);
|
|
11
|
-
} catch {
|
|
12
|
-
return "[Unserializable]";
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function getExecuteErrorMessage(error) {
|
|
18
|
-
if (error instanceof Error && typeof error.message === "string" && error.message.trim().length > 0) {
|
|
19
|
-
return error.message.trim();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!error || typeof error !== "object") {
|
|
23
|
-
return String(error);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const message = typeof error.message === "string" && error.message.trim().length > 0 ? error.message.trim() : typeof error.sqlMessage === "string" && error.sqlMessage.trim().length > 0 ? error.sqlMessage.trim() : "";
|
|
27
|
-
const detail = {};
|
|
28
|
-
|
|
29
|
-
if (typeof error.code === "string" && error.code.trim().length > 0) {
|
|
30
|
-
detail.code = error.code;
|
|
31
|
-
}
|
|
32
|
-
if (typeof error.errno === "number") {
|
|
33
|
-
detail.errno = error.errno;
|
|
34
|
-
}
|
|
35
|
-
if (typeof error.sqlState === "string" && error.sqlState.trim().length > 0) {
|
|
36
|
-
detail.sqlState = error.sqlState;
|
|
37
|
-
}
|
|
38
|
-
if (typeof error.sqlMessage === "string" && error.sqlMessage.trim().length > 0) {
|
|
39
|
-
detail.sqlMessage = error.sqlMessage;
|
|
40
|
-
}
|
|
41
|
-
if (typeof error.detail === "string" && error.detail.trim().length > 0) {
|
|
42
|
-
detail.detail = error.detail;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const detailText = Object.keys(detail).length > 0 ? safeStringify(detail) : safeStringify(error);
|
|
46
|
-
if (message && detailText && detailText !== "{}") {
|
|
47
|
-
return `${message} | ${detailText}`;
|
|
48
|
-
}
|
|
49
|
-
if (message) {
|
|
50
|
-
return message;
|
|
51
|
-
}
|
|
52
|
-
return detailText;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const executeMethods = {
|
|
56
|
-
async execute(sql, params) {
|
|
57
|
-
if (!this.sql) {
|
|
58
|
-
throw new Error("数据库连接未初始化", {
|
|
59
|
-
cause: null,
|
|
60
|
-
code: "runtime"
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
validateExecuteSql(sql);
|
|
65
|
-
|
|
66
|
-
const startTime = Date.now();
|
|
67
|
-
const safeParams = toSqlParams(params);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
let queryResult;
|
|
71
|
-
// Bun 事件循环 bug workaround:
|
|
72
|
-
// 当事件循环中只有一个 I/O 源(SQL socket)时,Bun 会走单源快速路径,
|
|
73
|
-
// 在 I/O 回调内直接唤醒 JS Promise,跳过正常调度,导致 SQL 连接状态
|
|
74
|
-
// 未完全重置,下一条查询的 Promise resolver 永久丢失(远程 TCP 连接必现)。
|
|
75
|
-
// 预注册一个 timer 使事件循环中同时存在两种事件源,强制 Bun 走正常多源调度路径。
|
|
76
|
-
// timer 时长无关紧要,查询完成后会被 clearTimeout 清理。
|
|
77
|
-
const _timer = setTimeout(() => {}, 2147483647);
|
|
78
|
-
const queryPromise = safeParams.length > 0 ? this.sql.unsafe(sql, safeParams) : this.sql.unsafe(sql);
|
|
79
|
-
try {
|
|
80
|
-
queryResult = await queryPromise;
|
|
81
|
-
} finally {
|
|
82
|
-
clearTimeout(_timer);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const duration = Date.now() - startTime;
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
data: queryResult,
|
|
89
|
-
sql: {
|
|
90
|
-
sql: sql,
|
|
91
|
-
params: safeParams,
|
|
92
|
-
duration: duration
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
} catch (error) {
|
|
96
|
-
const duration = Date.now() - startTime;
|
|
97
|
-
const msg = getExecuteErrorMessage(error);
|
|
98
|
-
const sqlInfo = {
|
|
99
|
-
sql: sql,
|
|
100
|
-
params: safeParams,
|
|
101
|
-
duration: duration
|
|
102
|
-
};
|
|
103
|
-
const wrappedError = new Error(`SQL执行失败: ${msg}`, {
|
|
104
|
-
cause: error
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
wrappedError.code = "runtime";
|
|
108
|
-
wrappedError.subsystem = "sql";
|
|
109
|
-
wrappedError.operation = "execute";
|
|
110
|
-
wrappedError.params = safeParams;
|
|
111
|
-
wrappedError.duration = duration;
|
|
112
|
-
wrappedError.sqlInfo = sqlInfo;
|
|
113
|
-
|
|
114
|
-
throw wrappedError;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
async tableExists(tableName) {
|
|
119
|
-
const snakeTableName = snakeCase(tableName);
|
|
120
|
-
const executeRes = await this.execute("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
|
|
121
|
-
const exists = (executeRes.data?.[0]?.count || 0) > 0;
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
data: exists,
|
|
125
|
-
sql: executeRes.sql
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
};
|