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.
@@ -0,0 +1,1626 @@
1
+ import { fieldClear } from "../utils/fieldClear.js";
2
+ import { isNonEmptyString, isNullable, isNumber, isPlainObject, isString } from "../utils/is.js";
3
+ import { arrayKeysToCamel, canConvertToNumber, keysToCamel, keysToSnake, snakeCase } from "../utils/util.js";
4
+ import { Logger } from "./logger.js";
5
+ import { SqlBuilder } from "./sqlBuilder.js";
6
+ import { addDefaultStateFilter, assertBatchInsertRowsConsistent, assertNoUndefinedInRecord, clearDeep, deserializeArrayFields, fieldsToSnake, orderByToSnake, parseLeftJoinItem, parseTableRef, processJoinField, processJoinOrderBy, processJoinWhere, serializeArrayFields, validateExplicitLeftJoinFields, validateExplicitReadFields, whereKeysToSnake } from "./dbUtil.js";
7
+
8
+ function quoteIdentMySql(identifier) {
9
+ if (!isString(identifier)) {
10
+ throw new Error(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`, {
11
+ cause: null,
12
+ code: "validation"
13
+ });
14
+ }
15
+
16
+ const trimmed = identifier.trim();
17
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
18
+ throw new Error(`无效的 SQL 标识符: ${trimmed}`, {
19
+ cause: null,
20
+ code: "validation"
21
+ });
22
+ }
23
+
24
+ return `\`${trimmed}\``;
25
+ }
26
+
27
+ function normalizeTableRef(tableRef) {
28
+ const parsed = parseTableRef(tableRef);
29
+ const schemaPart = parsed.schema ? snakeCase(parsed.schema) : null;
30
+ const tablePart = snakeCase(parsed.table);
31
+ const aliasPart = parsed.alias ? snakeCase(parsed.alias) : null;
32
+ let result = schemaPart ? `${schemaPart}.${tablePart}` : tablePart;
33
+ if (aliasPart) {
34
+ result = `${result} ${aliasPart}`;
35
+ }
36
+ return result;
37
+ }
38
+
39
+ function getJoinMainQualifier(tableRef) {
40
+ const parsed = parseTableRef(tableRef);
41
+ if (parsed.alias) {
42
+ return snakeCase(parsed.alias);
43
+ }
44
+ return snakeCase(parsed.table);
45
+ }
46
+
47
+ function normalizeSqlMetaNumber(value) {
48
+ if (isNullable(value)) {
49
+ return 0;
50
+ }
51
+
52
+ if (typeof value !== "number") {
53
+ return 0;
54
+ }
55
+
56
+ if (!Number.isFinite(value)) {
57
+ return 0;
58
+ }
59
+
60
+ return value;
61
+ }
62
+
63
+ function normalizeBigIntValues(value) {
64
+ if (isNullable(value)) {
65
+ return value;
66
+ }
67
+
68
+ const convertRecord = (source) => {
69
+ const converted = {};
70
+
71
+ for (const [key, item] of Object.entries(source)) {
72
+ let nextValue = item;
73
+
74
+ if (typeof item === "bigint") {
75
+ const convertedNumber = canConvertToNumber(item);
76
+ if (convertedNumber !== null) {
77
+ nextValue = convertedNumber;
78
+ } else {
79
+ nextValue = String(item);
80
+ }
81
+ }
82
+
83
+ converted[key] = nextValue;
84
+ }
85
+
86
+ return converted;
87
+ };
88
+
89
+ if (Array.isArray(value)) {
90
+ return value.map((item) => convertRecord(item));
91
+ }
92
+
93
+ if (typeof value === "object") {
94
+ return convertRecord(value);
95
+ }
96
+
97
+ return value;
98
+ }
99
+
100
+ function safeStringify(value) {
101
+ try {
102
+ return JSON.stringify(value);
103
+ } catch {
104
+ try {
105
+ return String(value);
106
+ } catch {
107
+ return "[Unserializable]";
108
+ }
109
+ }
110
+ }
111
+
112
+ function getExecuteErrorMessage(error) {
113
+ if (error instanceof Error && typeof error.message === "string" && error.message.trim().length > 0) {
114
+ return error.message.trim();
115
+ }
116
+
117
+ if (!error || typeof error !== "object") {
118
+ return String(error);
119
+ }
120
+
121
+ 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() : "";
122
+ const detail = {};
123
+
124
+ if (typeof error.code === "string" && error.code.trim().length > 0) {
125
+ detail.code = error.code;
126
+ }
127
+ if (typeof error.errno === "number") {
128
+ detail.errno = error.errno;
129
+ }
130
+ if (typeof error.sqlState === "string" && error.sqlState.trim().length > 0) {
131
+ detail.sqlState = error.sqlState;
132
+ }
133
+ if (typeof error.sqlMessage === "string" && error.sqlMessage.trim().length > 0) {
134
+ detail.sqlMessage = error.sqlMessage;
135
+ }
136
+ if (typeof error.detail === "string" && error.detail.trim().length > 0) {
137
+ detail.detail = error.detail;
138
+ }
139
+
140
+ const detailText = Object.keys(detail).length > 0 ? safeStringify(detail) : safeStringify(error);
141
+ if (message && detailText && detailText !== "{}") {
142
+ return `${message} | ${detailText}`;
143
+ }
144
+ if (message) {
145
+ return message;
146
+ }
147
+ return detailText;
148
+ }
149
+
150
+ function assertNonEmptyString(value, label) {
151
+ if (!isNonEmptyString(value)) {
152
+ throw new Error(`${label} 必须是非空字符串`, {
153
+ cause: null,
154
+ code: "validation"
155
+ });
156
+ }
157
+ }
158
+
159
+ function assertPlainObjectValue(value, label) {
160
+ if (!isPlainObject(value)) {
161
+ throw new Error(`${label} 必须是对象`, {
162
+ cause: null,
163
+ code: "validation"
164
+ });
165
+ }
166
+ }
167
+
168
+ function validateExcludeRules(excludeRules, label) {
169
+ if (isNullable(excludeRules)) {
170
+ return;
171
+ }
172
+
173
+ if (!isPlainObject(excludeRules)) {
174
+ throw new Error(`${label} 必须是对象`, {
175
+ cause: null,
176
+ code: "validation"
177
+ });
178
+ }
179
+
180
+ for (const [key, value] of Object.entries(excludeRules)) {
181
+ assertNonEmptyString(key, `${label} 的字段名`);
182
+ if (!Array.isArray(value)) {
183
+ throw new Error(`${label}.${key} 必须是数组`, {
184
+ cause: null,
185
+ code: "validation"
186
+ });
187
+ }
188
+ }
189
+ }
190
+
191
+ function validateWhereMeta(where, label) {
192
+ if (!isPlainObject(where)) {
193
+ return;
194
+ }
195
+
196
+ for (const [key, value] of Object.entries(where)) {
197
+ if (key === "$exclude") {
198
+ validateExcludeRules(value, `${label}.$exclude`);
199
+ continue;
200
+ }
201
+
202
+ if ((key === "$or" || key === "$and") && Array.isArray(value)) {
203
+ for (let i = 0; i < value.length; i++) {
204
+ if (isPlainObject(value[i])) {
205
+ validateWhereMeta(value[i], `${label}.${key}[${i}]`);
206
+ }
207
+ }
208
+ continue;
209
+ }
210
+
211
+ if (isPlainObject(value)) {
212
+ validateWhereMeta(value, `${label}.${key}`);
213
+ }
214
+ }
215
+ }
216
+
217
+ function validateWhereObject(where, label, required = false) {
218
+ if (isNullable(where)) {
219
+ if (required) {
220
+ throw new Error(`${label} 必须是对象`, {
221
+ cause: null,
222
+ code: "validation"
223
+ });
224
+ }
225
+ return;
226
+ }
227
+
228
+ if (!isPlainObject(where)) {
229
+ throw new Error(`${label} 必须是对象`, {
230
+ cause: null,
231
+ code: "validation"
232
+ });
233
+ }
234
+
235
+ validateWhereMeta(where, label);
236
+ }
237
+
238
+ function validateOrderByItems(orderBy, label) {
239
+ if (!Array.isArray(orderBy)) {
240
+ throw new Error(`${label} 必须是数组`, {
241
+ cause: null,
242
+ code: "validation"
243
+ });
244
+ }
245
+
246
+ for (const item of orderBy) {
247
+ if (!isString(item) || !item.includes("#")) {
248
+ throw new Error(`${label} 字段必须是 "字段#方向" 格式的字符串`, {
249
+ cause: null,
250
+ code: "validation"
251
+ });
252
+ }
253
+
254
+ const parts = item.split("#");
255
+ if (parts.length !== 2) {
256
+ throw new Error(`${label} 字段必须是 "字段#方向" 格式的字符串`, {
257
+ cause: null,
258
+ code: "validation"
259
+ });
260
+ }
261
+
262
+ const field = isString(parts[0]) ? parts[0].trim() : "";
263
+ const direction = isString(parts[1]) ? parts[1].trim().toUpperCase() : "";
264
+ if (!field) {
265
+ throw new Error(`${label} 中字段名不能为空`, {
266
+ cause: null,
267
+ code: "validation"
268
+ });
269
+ }
270
+ if (direction !== "ASC" && direction !== "DESC") {
271
+ throw new Error(`${label} 方向必须是 ASC 或 DESC`, {
272
+ cause: null,
273
+ code: "validation"
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ function validateJoinFieldRef(value, label) {
280
+ if (!isNonEmptyString(value)) {
281
+ throw new Error(`${label} 不能为空`, {
282
+ cause: null,
283
+ code: "validation"
284
+ });
285
+ }
286
+
287
+ const parts = value.split(".").map((item) => item.trim());
288
+ if (parts.length !== 2 || !isNonEmptyString(parts[0]) || !isNonEmptyString(parts[1])) {
289
+ throw new Error(`${label} 必须是 alias.field 格式`, {
290
+ cause: null,
291
+ code: "validation"
292
+ });
293
+ }
294
+ }
295
+
296
+ function parseLeftJoinEquality(joinItem) {
297
+ if (!isNonEmptyString(joinItem)) {
298
+ return null;
299
+ }
300
+
301
+ const match = joinItem.match(/^([a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*) ([a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*)$/);
302
+ if (!match) {
303
+ return null;
304
+ }
305
+
306
+ return {
307
+ left: match[1],
308
+ right: match[2]
309
+ };
310
+ }
311
+
312
+ function validateLeftJoinItems(leftJoin, label) {
313
+ if (!Array.isArray(leftJoin)) {
314
+ throw new Error(`${label} 必须是数组`, {
315
+ cause: null,
316
+ code: "validation"
317
+ });
318
+ }
319
+
320
+ for (let i = 0; i < leftJoin.length; i++) {
321
+ const joinItem = leftJoin[i];
322
+ if (!isNonEmptyString(joinItem)) {
323
+ throw new Error(`${label}[${i}] 必须是非空字符串`, {
324
+ cause: null,
325
+ code: "validation"
326
+ });
327
+ }
328
+
329
+ const parsed = parseLeftJoinEquality(joinItem);
330
+ if (!parsed) {
331
+ throw new Error(`${label}[${i}] 必须是 "left.field right.field" 格式(空格表示等于)`, {
332
+ cause: null,
333
+ code: "validation"
334
+ });
335
+ }
336
+
337
+ validateJoinFieldRef(parsed.left, `${label}[${i}] 左侧字段`);
338
+ validateJoinFieldRef(parsed.right, `${label}[${i}] 右侧字段`);
339
+ }
340
+ }
341
+
342
+ function validateLegacyJoinOptions(options, label) {
343
+ const legacyJoinKeys = ["joins", "innerJoin", "rightJoin"];
344
+ for (const key of legacyJoinKeys) {
345
+ if (!isNullable(options[key])) {
346
+ throw new Error(`${label} 仅支持 leftJoin,${label}.${key} 已废弃`, {
347
+ cause: null,
348
+ code: "validation"
349
+ });
350
+ }
351
+ }
352
+ }
353
+
354
+ function validateQueryTableOption(table, leftJoin, label) {
355
+ if (Array.isArray(table)) {
356
+ if (table.length === 0) {
357
+ throw new Error(`${label}.table 不能为空数组`, {
358
+ cause: null,
359
+ code: "validation"
360
+ });
361
+ }
362
+
363
+ for (let i = 0; i < table.length; i++) {
364
+ assertNonEmptyString(table[i], `${label}.table[${i}]`);
365
+ parseTableRef(table[i]);
366
+ }
367
+
368
+ if (Array.isArray(leftJoin) && leftJoin.length > 0) {
369
+ if (table.length !== leftJoin.length + 1) {
370
+ throw new Error(`${label}.table 与 ${label}.leftJoin 数量不匹配,要求 table.length = leftJoin.length + 1`, {
371
+ cause: null,
372
+ code: "validation"
373
+ });
374
+ }
375
+ return;
376
+ }
377
+
378
+ if (table.length > 1) {
379
+ throw new Error(`${label}.table 为数组时必须配合 leftJoin 使用`, {
380
+ cause: null,
381
+ code: "validation"
382
+ });
383
+ }
384
+ return;
385
+ }
386
+
387
+ assertNonEmptyString(table, `${label}.table`);
388
+ parseTableRef(table);
389
+
390
+ if (Array.isArray(leftJoin) && leftJoin.length > 0) {
391
+ throw new Error(`${label}.leftJoin 启用时,${label}.table 必须是数组`, {
392
+ cause: null,
393
+ code: "validation"
394
+ });
395
+ }
396
+ }
397
+
398
+ function validateQueryOptions(options, label) {
399
+ if (!isPlainObject(options)) {
400
+ throw new Error(`${label} 必须是对象`, {
401
+ cause: null,
402
+ code: "validation"
403
+ });
404
+ }
405
+
406
+ validateQueryTableOption(options.table, options.leftJoin, label);
407
+
408
+ if (!isNullable(options.where)) {
409
+ validateWhereObject(options.where, `${label}.where`);
410
+ }
411
+
412
+ if (!isNullable(options.fields) && !Array.isArray(options.fields)) {
413
+ throw new Error(`${label}.fields 必须是数组`, {
414
+ cause: null,
415
+ code: "validation"
416
+ });
417
+ }
418
+
419
+ if (!isNullable(options.orderBy)) {
420
+ validateOrderByItems(options.orderBy, `${label}.orderBy`);
421
+ }
422
+
423
+ validateLegacyJoinOptions(options, label);
424
+
425
+ if (!isNullable(options.leftJoin)) {
426
+ validateLeftJoinItems(options.leftJoin, `${label}.leftJoin`);
427
+ }
428
+
429
+ if (!isNullable(options.page) && (!Number.isFinite(options.page) || Math.floor(options.page) !== options.page)) {
430
+ throw new Error(`${label}.page 必须是整数`, {
431
+ cause: null,
432
+ code: "validation"
433
+ });
434
+ }
435
+
436
+ if (!isNullable(options.limit) && (!Number.isFinite(options.limit) || Math.floor(options.limit) !== options.limit)) {
437
+ throw new Error(`${label}.limit 必须是整数`, {
438
+ cause: null,
439
+ code: "validation"
440
+ });
441
+ }
442
+ }
443
+
444
+ function validateTableName(table, label) {
445
+ assertNonEmptyString(table, label);
446
+ }
447
+
448
+ function validateBatchDataList(dataList, label) {
449
+ if (!Array.isArray(dataList)) {
450
+ throw new Error(`${label} 必须是数组`, {
451
+ cause: null,
452
+ code: "validation"
453
+ });
454
+ }
455
+
456
+ for (let i = 0; i < dataList.length; i++) {
457
+ if (!isPlainObject(dataList[i])) {
458
+ throw new Error(`${label}[${i}] 必须是对象`, {
459
+ cause: null,
460
+ code: "validation"
461
+ });
462
+ }
463
+ }
464
+ }
465
+
466
+ function validateTableWhereOptions(options, label, required = false) {
467
+ validateTableName(options.table, `${label}.table`);
468
+ validateWhereObject(options.where, `${label}.where`, required);
469
+ }
470
+
471
+ function validateTableDataOptions(options, label) {
472
+ validateTableName(options.table, `${label}.table`);
473
+ assertPlainObjectValue(options.data, `${label}.data`);
474
+ }
475
+
476
+ function validateTableBatchDataOptions(table, dataList, label) {
477
+ validateTableName(table, `${label}.table`);
478
+ validateBatchDataList(dataList, `${label}.dataList`);
479
+ }
480
+
481
+ function validateSimpleTableName(rawTable, label) {
482
+ if (Array.isArray(rawTable)) {
483
+ throw new Error(`${label}.table 不支持数组`, {
484
+ cause: null,
485
+ code: "validation"
486
+ });
487
+ }
488
+
489
+ const table = isString(rawTable) ? rawTable.trim() : "";
490
+ if (!table) {
491
+ throw new Error(`${label}.table 不能为空`, {
492
+ cause: null,
493
+ code: "validation"
494
+ });
495
+ }
496
+ if (table.includes(" ")) {
497
+ throw new Error(`${label} 不支持别名表写法(table: ${table})`, {
498
+ cause: null,
499
+ code: "validation"
500
+ });
501
+ }
502
+ if (table.includes(".")) {
503
+ throw new Error(`${label} 不支持 schema.table 写法(table: ${table})`, {
504
+ cause: null,
505
+ code: "validation"
506
+ });
507
+ }
508
+ }
509
+
510
+ function validateNoLeftJoin(leftJoin, message) {
511
+ if (Array.isArray(leftJoin) && leftJoin.length > 0) {
512
+ throw new Error(message, {
513
+ cause: null,
514
+ code: "validation"
515
+ });
516
+ }
517
+ }
518
+
519
+ function validateNoLeftJoinReadOptions(options, label, joinErrorMessage) {
520
+ validateTableName(options.table, `${label}.table`);
521
+ validateWhereObject(options.where, `${label}.where`, false);
522
+ validateNoLeftJoin(options.leftJoin, joinErrorMessage);
523
+ validateLegacyJoinOptions(options, label);
524
+ validateSimpleTableName(options.table, label);
525
+ }
526
+
527
+ function validateSafeFieldName(field) {
528
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
529
+ throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线`, {
530
+ cause: null,
531
+ code: "validation"
532
+ });
533
+ }
534
+ }
535
+
536
+ function validatePageLimitRange(prepared, rawTable) {
537
+ if (prepared.page < 1 || prepared.page > 10000) {
538
+ throw new Error(`页码必须在 1 到 10000 之间 (table: ${rawTable}, page: ${prepared.page}, limit: ${prepared.limit})`, {
539
+ cause: null,
540
+ code: "validation"
541
+ });
542
+ }
543
+ if (prepared.limit < 1 || prepared.limit > 100000) {
544
+ throw new Error(`每页数量必须在 1 到 100000 之间 (table: ${rawTable}, page: ${prepared.page}, limit: ${prepared.limit})`, {
545
+ cause: null,
546
+ code: "validation"
547
+ });
548
+ }
549
+ }
550
+
551
+ function validateGeneratedBatchId(id, table, index) {
552
+ if (typeof id !== "number") {
553
+ throw new Error(`批量插入生成 ID 失败:ids[${index}] 不是 number (table: ${table})`, {
554
+ cause: null,
555
+ code: "runtime"
556
+ });
557
+ }
558
+ }
559
+
560
+ function validateTimeIdValue(id) {
561
+ if (!Number.isFinite(id) || id <= 0) {
562
+ throw new Error(`buildInsertRow(timeId) 失败:id 必须为 > 0 的有限 number (id: ${String(id)})`, {
563
+ cause: null,
564
+ code: "validation"
565
+ });
566
+ }
567
+ }
568
+
569
+ function validateInsertBatchSize(count, maxBatchSize) {
570
+ if (count > maxBatchSize) {
571
+ throw new Error(`批量插入数量 ${count} 超过最大限制 ${maxBatchSize}`, {
572
+ cause: null,
573
+ code: "validation"
574
+ });
575
+ }
576
+ }
577
+
578
+ function validateIncrementValue(table, field, value) {
579
+ if (typeof value !== "number" || Number.isNaN(value)) {
580
+ throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`, {
581
+ cause: null,
582
+ code: "validation"
583
+ });
584
+ }
585
+ }
586
+
587
+ function validateIncrementOptions(table, field, where, value, label) {
588
+ validateTableName(table, `${label}.table`);
589
+ validateWhereObject(where, `${label}.where`, true);
590
+ validateSafeFieldName(snakeCase(table));
591
+ validateSafeFieldName(snakeCase(field));
592
+ validateIncrementValue(table, field, value);
593
+ }
594
+
595
+ function validateIdList(ids, label) {
596
+ if (!Array.isArray(ids)) {
597
+ throw new Error(`${label} 必须是数组`, {
598
+ cause: null,
599
+ code: "validation"
600
+ });
601
+ }
602
+ }
603
+
604
+ function validateEffectiveWhere(snakeWhere, label) {
605
+ if (!snakeWhere || Object.keys(snakeWhere).length === 0) {
606
+ throw new Error(`${label} 需要有效的 WHERE 条件`, {
607
+ cause: null,
608
+ code: "validation"
609
+ });
610
+ }
611
+ }
612
+
613
+ function validateDataReadOptions(options, label) {
614
+ validateQueryOptions(options, label);
615
+
616
+ const hasLeftJoin = Array.isArray(options.leftJoin) && options.leftJoin.length > 0;
617
+ const classifiedFields = hasLeftJoin ? validateExplicitLeftJoinFields(options.fields) : validateExplicitReadFields(options.fields);
618
+
619
+ return {
620
+ hasLeftJoin: hasLeftJoin,
621
+ classifiedFields: classifiedFields
622
+ };
623
+ }
624
+
625
+ function validateCountReadOptions(options, label) {
626
+ validateQueryOptions(options, label);
627
+
628
+ return {
629
+ hasLeftJoin: Array.isArray(options.leftJoin) && options.leftJoin.length > 0
630
+ };
631
+ }
632
+
633
+ function validateGetCountOptions(options) {
634
+ return validateCountReadOptions(options, "getCount.options");
635
+ }
636
+
637
+ function validateGetOneOptions(options) {
638
+ return validateDataReadOptions(options, "getOne.options");
639
+ }
640
+
641
+ function validateGetListOptions(options) {
642
+ return validateDataReadOptions(options, "getList.options");
643
+ }
644
+
645
+ function validateGetAllOptions(options) {
646
+ return validateDataReadOptions(options, "getAll.options");
647
+ }
648
+
649
+ class DbHelper {
650
+ constructor(options = {}) {
651
+ this.redis = options.redis;
652
+
653
+ if (!isNonEmptyString(options.dbName)) {
654
+ throw new Error("DbHelper 初始化失败:dbName 必须为非空字符串", {
655
+ cause: null,
656
+ code: "validation"
657
+ });
658
+ }
659
+ this.dbName = options.dbName;
660
+
661
+ this.sql = options.sql || null;
662
+ this.isTransaction = options.isTransaction === true;
663
+ this.beflyMode = options.beflyMode === "manual" ? "manual" : "auto";
664
+ }
665
+
666
+ async execute(sql, params) {
667
+ if (!this.sql) {
668
+ throw new Error("数据库连接未初始化", {
669
+ cause: null,
670
+ code: "runtime"
671
+ });
672
+ }
673
+
674
+ if (!isString(sql)) {
675
+ throw new Error(`execute 只接受字符串类型的 SQL,收到类型: ${typeof sql},值: ${JSON.stringify(sql)}`, {
676
+ cause: null,
677
+ code: "validation"
678
+ });
679
+ }
680
+
681
+ const startTime = Date.now();
682
+ let safeParams = [];
683
+ if (!isNullable(params)) {
684
+ if (!Array.isArray(params)) {
685
+ throw new Error(`SQL 参数必须是数组,当前类型: ${typeof params}`, {
686
+ cause: null,
687
+ code: "validation"
688
+ });
689
+ }
690
+
691
+ safeParams = [];
692
+ for (const value of params) {
693
+ if (value === undefined) {
694
+ throw new Error("SQL 参数不能为 undefined", {
695
+ cause: null,
696
+ code: "validation"
697
+ });
698
+ }
699
+
700
+ if (typeof value === "bigint") {
701
+ safeParams.push(value.toString());
702
+ continue;
703
+ }
704
+
705
+ safeParams.push(value);
706
+ }
707
+ }
708
+
709
+ try {
710
+ let queryResult;
711
+ const timer = setTimeout(() => {}, 2147483647);
712
+ const queryPromise = safeParams.length > 0 ? this.sql.unsafe(sql, safeParams) : this.sql.unsafe(sql);
713
+ try {
714
+ queryResult = await queryPromise;
715
+ } finally {
716
+ clearTimeout(timer);
717
+ }
718
+
719
+ const duration = Date.now() - startTime;
720
+
721
+ return {
722
+ data: queryResult,
723
+ sql: {
724
+ sql: sql,
725
+ params: safeParams,
726
+ duration: duration
727
+ }
728
+ };
729
+ } catch (error) {
730
+ const duration = Date.now() - startTime;
731
+ const msg = getExecuteErrorMessage(error);
732
+ const sqlInfo = {
733
+ sql: sql,
734
+ params: safeParams,
735
+ duration: duration
736
+ };
737
+ const wrappedError = new Error(`SQL执行失败: ${msg}`, {
738
+ cause: error
739
+ });
740
+
741
+ wrappedError.code = "runtime";
742
+ wrappedError.subsystem = "sql";
743
+ wrappedError.operation = "execute";
744
+ wrappedError.params = safeParams;
745
+ wrappedError.duration = duration;
746
+ wrappedError.sqlInfo = sqlInfo;
747
+
748
+ throw wrappedError;
749
+ }
750
+ }
751
+
752
+ async tableExists(tableName) {
753
+ const snakeTableName = snakeCase(tableName);
754
+ const executeRes = await this.execute("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
755
+ const exists = (executeRes.data?.[0]?.count || 0) > 0;
756
+
757
+ return {
758
+ data: exists,
759
+ sql: executeRes.sql
760
+ };
761
+ }
762
+
763
+ createSqlBuilder() {
764
+ return new SqlBuilder({ quoteIdent: quoteIdentMySql });
765
+ }
766
+
767
+ createListBuilder(prepared, whereFiltered, limit, offset) {
768
+ const builder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(limit);
769
+ this.applyLeftJoins(builder, prepared.leftJoins);
770
+ if (!isNullable(offset)) {
771
+ builder.offset(offset);
772
+ }
773
+ if (prepared.orderBy && prepared.orderBy.length > 0) {
774
+ builder.orderBy(prepared.orderBy);
775
+ }
776
+ return builder;
777
+ }
778
+
779
+ async fetchCount(prepared, whereFiltered, alias) {
780
+ const builder = this.createSqlBuilder().selectRaw(alias).from(prepared.table).where(whereFiltered);
781
+ this.applyLeftJoins(builder, prepared.leftJoins);
782
+ const result = builder.toSelectSql();
783
+ const executeRes = await this.execute(result.sql, result.params);
784
+ const countRow = executeRes.data?.[0] || null;
785
+ const normalizedCountRow = countRow ? normalizeBigIntValues(countRow) : null;
786
+ const total = normalizedCountRow?.total ?? normalizedCountRow?.count ?? 0;
787
+ return {
788
+ total: total,
789
+ sql: executeRes.sql
790
+ };
791
+ }
792
+
793
+ normalizeRowData(row) {
794
+ if (!row) {
795
+ return {};
796
+ }
797
+
798
+ const camelRow = keysToCamel(row);
799
+ const deserialized = deserializeArrayFields(camelRow);
800
+ if (!deserialized) {
801
+ return {};
802
+ }
803
+
804
+ const convertedList = normalizeBigIntValues([deserialized]);
805
+ if (convertedList && convertedList.length > 0) {
806
+ return convertedList[0];
807
+ }
808
+
809
+ return deserialized;
810
+ }
811
+
812
+ normalizeListData(list) {
813
+ const camelList = arrayKeysToCamel(list);
814
+ const deserializedList = camelList.map((item) => deserializeArrayFields(item)).filter((item) => item !== null);
815
+ return normalizeBigIntValues(deserializedList);
816
+ }
817
+
818
+ _joinFieldsToSnake(classifiedFields) {
819
+ if (classifiedFields.type === "include") {
820
+ return classifiedFields.fields.map((field) => processJoinField(field));
821
+ }
822
+
823
+ return [];
824
+ }
825
+
826
+ _cleanAndSnakeAndSerializeWriteData(data, excludeValues = [null, undefined]) {
827
+ const cleanData = fieldClear(data, { excludeValues: excludeValues });
828
+ return serializeArrayFields(keysToSnake(cleanData));
829
+ }
830
+
831
+ _stripSystemFieldsForWrite(data, allowState) {
832
+ const result = {};
833
+ for (const [key, value] of Object.entries(data)) {
834
+ if (key === "id") continue;
835
+ if (key === "created_at") continue;
836
+ if (key === "updated_at") continue;
837
+ if (key === "deleted_at") continue;
838
+ if (!allowState && key === "state") continue;
839
+ result[key] = value;
840
+ }
841
+ return result;
842
+ }
843
+
844
+ _prepareWriteUserData(data, allowState) {
845
+ return this._stripSystemFieldsForWrite(this._cleanAndSnakeAndSerializeWriteData(data), allowState);
846
+ }
847
+
848
+ _buildInsertRow(options) {
849
+ const result = { ...this._prepareWriteUserData(options.data, false) };
850
+
851
+ if (options.beflyMode === "auto") {
852
+ validateTimeIdValue(options.id);
853
+ result.id = options.id;
854
+ }
855
+
856
+ if (options.beflyMode !== "manual") {
857
+ result.created_at = options.now;
858
+ result.updated_at = options.now;
859
+ result.state = 1;
860
+ }
861
+
862
+ return result;
863
+ }
864
+
865
+ _buildUpdateRow(options) {
866
+ const result = { ...this._prepareWriteUserData(options.data, options.allowState) };
867
+ if (options.beflyMode !== "manual") {
868
+ result.updated_at = options.now;
869
+ }
870
+ return result;
871
+ }
872
+
873
+ _buildPartialUpdateData(options) {
874
+ return this._prepareWriteUserData(options.data, options.allowState);
875
+ }
876
+
877
+ normalizeLeftJoinOptions(options, cleanWhere, classifiedFields) {
878
+ const processedFields = this._joinFieldsToSnake(classifiedFields);
879
+ const mainTableRef = options.table[0];
880
+ const joinTableRefs = options.table.slice(1);
881
+ const normalizedTableRef = normalizeTableRef(mainTableRef);
882
+ const mainQualifier = getJoinMainQualifier(mainTableRef);
883
+ const leftJoins = options.leftJoin.map((joinItem, index) => parseLeftJoinItem(joinTableRefs[index], joinItem));
884
+
885
+ return {
886
+ table: normalizedTableRef,
887
+ tableQualifier: mainQualifier,
888
+ fields: processedFields,
889
+ where: processJoinWhere(cleanWhere),
890
+ leftJoins: leftJoins,
891
+ orderBy: processJoinOrderBy(options.orderBy || []),
892
+ page: options.page || 1,
893
+ limit: options.limit || 10
894
+ };
895
+ }
896
+
897
+ normalizeLeftJoinCountOptions(options, cleanWhere) {
898
+ const mainTableRef = options.table[0];
899
+ const joinTableRefs = options.table.slice(1);
900
+ const normalizedTableRef = normalizeTableRef(mainTableRef);
901
+ const mainQualifier = getJoinMainQualifier(mainTableRef);
902
+ const leftJoins = options.leftJoin.map((joinItem, index) => parseLeftJoinItem(joinTableRefs[index], joinItem));
903
+
904
+ return {
905
+ table: normalizedTableRef,
906
+ tableQualifier: mainQualifier,
907
+ fields: [],
908
+ where: processJoinWhere(cleanWhere),
909
+ leftJoins: leftJoins,
910
+ orderBy: [],
911
+ page: options.page || 1,
912
+ limit: options.limit || 10
913
+ };
914
+ }
915
+
916
+ async normalizeSingleTableOptions(options, cleanWhere, classifiedFields) {
917
+ const tableRef = Array.isArray(options.table) ? options.table[0] : options.table;
918
+ const snakeTable = snakeCase(tableRef);
919
+ const processedFields = await fieldsToSnake(snakeTable, classifiedFields, this.getTableColumns.bind(this));
920
+
921
+ return {
922
+ table: snakeTable,
923
+ tableQualifier: snakeTable,
924
+ fields: processedFields,
925
+ where: whereKeysToSnake(cleanWhere),
926
+ leftJoins: [],
927
+ orderBy: orderByToSnake(options.orderBy || []),
928
+ page: options.page || 1,
929
+ limit: options.limit || 10
930
+ };
931
+ }
932
+
933
+ buildQueryOptions(options, defaults) {
934
+ const output = {
935
+ table: options.table,
936
+ page: defaults.page,
937
+ limit: defaults.limit
938
+ };
939
+
940
+ if (options.fields !== undefined) output.fields = options.fields;
941
+ if (options.where !== undefined) output.where = options.where;
942
+ if (options.leftJoin !== undefined) output.leftJoin = options.leftJoin;
943
+ if (options.orderBy !== undefined) output.orderBy = options.orderBy;
944
+ return output;
945
+ }
946
+
947
+ resolveFieldValue(record, field) {
948
+ if (!isPlainObject(record)) {
949
+ return null;
950
+ }
951
+
952
+ if (Object.hasOwn(record, field)) {
953
+ return record[field];
954
+ }
955
+
956
+ const camelField = field.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
957
+ if (camelField !== field && Object.hasOwn(record, camelField)) {
958
+ return record[camelField];
959
+ }
960
+
961
+ const snakeField = field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
962
+ if (snakeField !== field && Object.hasOwn(record, snakeField)) {
963
+ return record[snakeField];
964
+ }
965
+
966
+ return null;
967
+ }
968
+
969
+ async getTableColumns(table) {
970
+ const parsed = parseTableRef(table);
971
+ const schemaPart = parsed.schema ? `${quoteIdentMySql(snakeCase(parsed.schema))}.` : "";
972
+ const quotedTable = quoteIdentMySql(snakeCase(parsed.table));
973
+ const executeRes = await this.execute(`SHOW COLUMNS FROM ${schemaPart}${quotedTable}`, []);
974
+ const result = executeRes.data;
975
+
976
+ if (!result || result.length === 0) {
977
+ throw new Error(`表 ${table} 不存在或没有字段`, {
978
+ cause: null,
979
+ code: "runtime"
980
+ });
981
+ }
982
+
983
+ const columnNames = [];
984
+ for (const row of result) {
985
+ const record = row;
986
+ const name = record.Field;
987
+ if (isNonEmptyString(name)) {
988
+ columnNames.push(name);
989
+ }
990
+ }
991
+
992
+ return columnNames;
993
+ }
994
+
995
+ applyLeftJoins(builder, leftJoins) {
996
+ if (!leftJoins || leftJoins.length === 0) {
997
+ return;
998
+ }
999
+
1000
+ for (const join of leftJoins) {
1001
+ builder.leftJoin(join.table, join.on);
1002
+ }
1003
+ }
1004
+
1005
+ async prepareBaseReadContext(options, validation, buildPrepared) {
1006
+ const cleanWhere = clearDeep(options.where || {});
1007
+ const prepared = await buildPrepared.call(this, options, cleanWhere, validation);
1008
+
1009
+ return {
1010
+ prepared: prepared,
1011
+ whereFiltered: addDefaultStateFilter(prepared.where, prepared.tableQualifier, prepared.leftJoins.length > 0, this.beflyMode)
1012
+ };
1013
+ }
1014
+
1015
+ async buildPreparedReadOptions(options, cleanWhere, validation) {
1016
+ if (validation.hasLeftJoin) {
1017
+ return this.normalizeLeftJoinOptions(options, cleanWhere, validation.classifiedFields);
1018
+ }
1019
+
1020
+ return await this.normalizeSingleTableOptions(options, cleanWhere, validation.classifiedFields);
1021
+ }
1022
+
1023
+ buildPreparedCountOptions(options, cleanWhere, validation) {
1024
+ if (validation.hasLeftJoin) {
1025
+ return this.normalizeLeftJoinCountOptions(options, cleanWhere);
1026
+ }
1027
+
1028
+ const snakeTable = snakeCase(Array.isArray(options.table) ? options.table[0] : options.table);
1029
+ return {
1030
+ table: snakeTable,
1031
+ tableQualifier: snakeTable,
1032
+ fields: [],
1033
+ where: whereKeysToSnake(cleanWhere),
1034
+ leftJoins: [],
1035
+ orderBy: [],
1036
+ page: options.page || 1,
1037
+ limit: options.limit || 10
1038
+ };
1039
+ }
1040
+
1041
+ async prepareReadContext(options, validation) {
1042
+ return await this.prepareBaseReadContext(options, validation, this.buildPreparedReadOptions);
1043
+ }
1044
+
1045
+ async prepareCountContext(options, validation) {
1046
+ return await this.prepareBaseReadContext(options, validation, this.buildPreparedCountOptions);
1047
+ }
1048
+
1049
+ prepareSingleTableWhere(table, where, useDefaultStateFilter = true) {
1050
+ const snakeTable = snakeCase(table);
1051
+ const snakeWhere = whereKeysToSnake(clearDeep(where || {}));
1052
+
1053
+ return {
1054
+ snakeTable: snakeTable,
1055
+ whereFiltered: useDefaultStateFilter ? addDefaultStateFilter(snakeWhere, snakeTable, false, this.beflyMode) : snakeWhere
1056
+ };
1057
+ }
1058
+
1059
+ prepareRequiredSingleTableWhere(table, where, useDefaultStateFilter, label) {
1060
+ const snakeTable = snakeCase(table);
1061
+ const snakeWhere = whereKeysToSnake(clearDeep(where || {}));
1062
+ validateEffectiveWhere(snakeWhere, label);
1063
+
1064
+ return {
1065
+ snakeTable: snakeTable,
1066
+ whereFiltered: useDefaultStateFilter ? addDefaultStateFilter(snakeWhere, snakeTable, false, this.beflyMode) : snakeWhere
1067
+ };
1068
+ }
1069
+
1070
+ async createInsertRows(table, snakeTable, dataList, now) {
1071
+ if (this.beflyMode === "manual") {
1072
+ return {
1073
+ ids: [],
1074
+ processedList: dataList.map((data) =>
1075
+ this._buildInsertRow({
1076
+ data: data,
1077
+ now: now,
1078
+ beflyMode: this.beflyMode
1079
+ })
1080
+ )
1081
+ };
1082
+ }
1083
+
1084
+ const ids = [];
1085
+ try {
1086
+ for (let i = 0; i < dataList.length; i++) {
1087
+ ids.push(await this.redis.genTimeID());
1088
+ }
1089
+ } catch (error) {
1090
+ if (dataList.length === 1) {
1091
+ throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, {
1092
+ cause: error,
1093
+ code: "runtime",
1094
+ subsystem: "db",
1095
+ operation: "genTimeId",
1096
+ table: table
1097
+ });
1098
+ }
1099
+ throw error;
1100
+ }
1101
+
1102
+ const processedList = dataList.map((data, index) => {
1103
+ const id = ids[index];
1104
+ if (dataList.length > 1) {
1105
+ validateGeneratedBatchId(id, snakeTable, index);
1106
+ }
1107
+
1108
+ return this._buildInsertRow({
1109
+ data: data,
1110
+ id: id,
1111
+ now: now,
1112
+ beflyMode: this.beflyMode
1113
+ });
1114
+ });
1115
+
1116
+ return {
1117
+ ids: ids,
1118
+ processedList: processedList
1119
+ };
1120
+ }
1121
+
1122
+ resolveInsertedData(table, count, executeRes, generatedIds, operation) {
1123
+ const lastInsertRowidNum = normalizeSqlMetaNumber(executeRes.data?.lastInsertRowid);
1124
+
1125
+ if (this.beflyMode === "manual") {
1126
+ if (lastInsertRowidNum <= 0) {
1127
+ throw new Error(operation === "insBatch" ? `批量插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})` : `插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})`, {
1128
+ cause: null,
1129
+ code: "runtime"
1130
+ });
1131
+ }
1132
+
1133
+ if (count === 1) {
1134
+ return lastInsertRowidNum;
1135
+ }
1136
+
1137
+ const outIds = [];
1138
+ for (let i = 0; i < count; i++) {
1139
+ outIds.push(lastInsertRowidNum + i);
1140
+ }
1141
+ return outIds;
1142
+ }
1143
+
1144
+ if (count === 1) {
1145
+ const generatedId = generatedIds[0];
1146
+ return (isNumber(generatedId) ? generatedId : 0) || lastInsertRowidNum || 0;
1147
+ }
1148
+
1149
+ return generatedIds;
1150
+ }
1151
+
1152
+ async getCount(options) {
1153
+ const validation = validateGetCountOptions(options);
1154
+ const { prepared, whereFiltered } = await this.prepareCountContext(options, validation);
1155
+ const result = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as count");
1156
+
1157
+ return {
1158
+ data: result.total,
1159
+ sql: result.sql
1160
+ };
1161
+ }
1162
+
1163
+ async getOne(options) {
1164
+ const validation = validateGetOneOptions(options);
1165
+ const { prepared, whereFiltered } = await this.prepareReadContext(options, validation);
1166
+ const builder = this.createSqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered);
1167
+ this.applyLeftJoins(builder, prepared.leftJoins);
1168
+
1169
+ const { sql, params } = builder.toSelectSql();
1170
+ const executeRes = await this.execute(sql, params);
1171
+ const result = executeRes.data;
1172
+
1173
+ const data = this.normalizeRowData(result?.[0] || null);
1174
+ return {
1175
+ data: data,
1176
+ sql: executeRes.sql
1177
+ };
1178
+ }
1179
+
1180
+ async getList(options) {
1181
+ const validation = validateGetListOptions(options);
1182
+ const { prepared, whereFiltered } = await this.prepareReadContext(options, validation);
1183
+ validatePageLimitRange(prepared, options.table);
1184
+ const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
1185
+ const total = countResult.total;
1186
+
1187
+ const offset = (prepared.page - 1) * prepared.limit;
1188
+ const dataBuilder = this.createListBuilder(prepared, whereFiltered, prepared.limit, offset);
1189
+ const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
1190
+
1191
+ if (total === 0) {
1192
+ return {
1193
+ data: {
1194
+ lists: [],
1195
+ total: 0,
1196
+ page: prepared.page,
1197
+ limit: prepared.limit,
1198
+ pages: 0
1199
+ },
1200
+ sql: {
1201
+ count: countResult.sql,
1202
+ data: {
1203
+ sql: dataSql,
1204
+ params: dataParams,
1205
+ duration: 0
1206
+ }
1207
+ }
1208
+ };
1209
+ }
1210
+ const listExecuteRes = await this.execute(dataSql, dataParams);
1211
+ const list = listExecuteRes.data || [];
1212
+
1213
+ return {
1214
+ data: {
1215
+ lists: this.normalizeListData(list),
1216
+ total: total,
1217
+ page: prepared.page,
1218
+ limit: prepared.limit,
1219
+ pages: Math.ceil(total / prepared.limit)
1220
+ },
1221
+ sql: {
1222
+ count: countResult.sql,
1223
+ data: listExecuteRes.sql
1224
+ }
1225
+ };
1226
+ }
1227
+
1228
+ async getAll(options) {
1229
+ const MAX_LIMIT = 100000;
1230
+ const WARNING_LIMIT = 10000;
1231
+ const prepareOptions = this.buildQueryOptions(options, { page: 1, limit: 10 });
1232
+ const validation = validateGetAllOptions(prepareOptions);
1233
+ const { prepared, whereFiltered } = await this.prepareReadContext(prepareOptions, validation);
1234
+ const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
1235
+ const total = countResult.total;
1236
+
1237
+ if (total === 0) {
1238
+ return {
1239
+ data: {
1240
+ lists: [],
1241
+ total: 0
1242
+ },
1243
+ sql: {
1244
+ count: countResult.sql
1245
+ }
1246
+ };
1247
+ }
1248
+
1249
+ const dataBuilder = this.createListBuilder(prepared, whereFiltered, MAX_LIMIT, null);
1250
+
1251
+ const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
1252
+ const listExecuteRes = await this.execute(dataSql, dataParams);
1253
+ const result = listExecuteRes.data || [];
1254
+
1255
+ if (result.length >= WARNING_LIMIT) {
1256
+ Logger.warn("getAll 返回数据过多,建议使用 getList 分页查询", { table: options.table, count: result.length, total: total });
1257
+ }
1258
+
1259
+ if (result.length >= MAX_LIMIT) {
1260
+ Logger.warn(`getAll 达到最大限制 ${MAX_LIMIT},实际总数 ${total},只返回前 ${MAX_LIMIT} 条`, { table: options.table, limit: MAX_LIMIT, total: total });
1261
+ }
1262
+
1263
+ const lists = this.normalizeListData(result);
1264
+
1265
+ return {
1266
+ data: {
1267
+ lists: lists,
1268
+ total: total
1269
+ },
1270
+ sql: {
1271
+ count: countResult.sql,
1272
+ data: listExecuteRes.sql
1273
+ }
1274
+ };
1275
+ }
1276
+
1277
+ async exists(options) {
1278
+ validateNoLeftJoinReadOptions(options, "exists", "exists 不支持 leftJoin(请使用显式 query 或拆分查询)");
1279
+ const prepared = this.prepareSingleTableWhere(options.table, options.where, true);
1280
+
1281
+ const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(prepared.snakeTable).where(prepared.whereFiltered).limit(1);
1282
+ const { sql, params } = builder.toSelectSql();
1283
+ const executeRes = await this.execute(sql, params);
1284
+ const exists = (executeRes.data?.[0]?.cnt || 0) > 0;
1285
+ return { data: exists, sql: executeRes.sql };
1286
+ }
1287
+
1288
+ async getFieldValue(options) {
1289
+ validateNoLeftJoinReadOptions(options, "getFieldValue", "getFieldValue 不支持 leftJoin(请使用 getOne/getList 并自行取字段)");
1290
+ const field = options.field;
1291
+ validateSafeFieldName(field);
1292
+
1293
+ const oneOptions = {
1294
+ table: options.table
1295
+ };
1296
+ if (options.where !== undefined) oneOptions.where = options.where;
1297
+ if (options.leftJoin !== undefined) oneOptions.leftJoin = options.leftJoin;
1298
+ if (options.orderBy !== undefined) oneOptions.orderBy = options.orderBy;
1299
+ if (options.page !== undefined) oneOptions.page = options.page;
1300
+ if (options.limit !== undefined) oneOptions.limit = options.limit;
1301
+ oneOptions.fields = [field];
1302
+
1303
+ const oneRes = await this.getOne(oneOptions);
1304
+
1305
+ const result = oneRes.data;
1306
+ const value = this.resolveFieldValue(result, field);
1307
+ return {
1308
+ data: value,
1309
+ sql: oneRes.sql
1310
+ };
1311
+ }
1312
+
1313
+ async insData(options) {
1314
+ validateTableDataOptions(options, "insData");
1315
+ const { table, data } = options;
1316
+ const snakeTable = snakeCase(table);
1317
+ const now = Date.now();
1318
+ const insertRows = await this.createInsertRows(table, snakeTable, [data], now);
1319
+ const processed = insertRows.processedList[0];
1320
+
1321
+ assertNoUndefinedInRecord(processed, `insData 插入数据 (table: ${snakeTable})`);
1322
+
1323
+ const builder = this.createSqlBuilder();
1324
+ const { sql, params } = builder.toInsertSql(snakeTable, processed);
1325
+ const executeRes = await this.execute(sql, params);
1326
+
1327
+ return {
1328
+ data: this.resolveInsertedData(table, 1, executeRes, insertRows.ids, "insData"),
1329
+ sql: executeRes.sql
1330
+ };
1331
+ }
1332
+
1333
+ async insBatch(table, dataList) {
1334
+ validateTableBatchDataOptions(table, dataList, "insBatch");
1335
+ if (dataList.length === 0) {
1336
+ return {
1337
+ data: [],
1338
+ sql: { sql: "", params: [], duration: 0 }
1339
+ };
1340
+ }
1341
+
1342
+ const MAX_BATCH_SIZE = 1000;
1343
+ validateInsertBatchSize(dataList.length, MAX_BATCH_SIZE);
1344
+
1345
+ const snakeTable = snakeCase(table);
1346
+ const now = Date.now();
1347
+ const insertRows = await this.createInsertRows(table, snakeTable, dataList, now);
1348
+ const processedList = insertRows.processedList;
1349
+
1350
+ const insertFields = assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
1351
+ const builder = this.createSqlBuilder();
1352
+ const { sql, params } = builder.toInsertSql(snakeTable, processedList);
1353
+
1354
+ try {
1355
+ const executeRes = await this.execute(sql, params);
1356
+ return {
1357
+ data: this.resolveInsertedData(table, dataList.length, executeRes, insertRows.ids, "insBatch"),
1358
+ sql: executeRes.sql
1359
+ };
1360
+ } catch (error) {
1361
+ Logger.error("批量插入失败", error, {
1362
+ table: table,
1363
+ snakeTable: snakeTable,
1364
+ count: dataList.length,
1365
+ fields: insertFields
1366
+ });
1367
+ throw new Error("批量插入失败", {
1368
+ cause: error,
1369
+ code: "runtime",
1370
+ subsystem: "sql",
1371
+ operation: "insBatch",
1372
+ table: table,
1373
+ snakeTable: snakeTable,
1374
+ count: dataList.length,
1375
+ fields: insertFields
1376
+ });
1377
+ }
1378
+ }
1379
+
1380
+ async delForceBatch(table, ids) {
1381
+ validateTableName(table, "delForceBatch.table");
1382
+ validateIdList(ids, "delForceBatch.ids");
1383
+ if (ids.length === 0) {
1384
+ return {
1385
+ data: 0,
1386
+ sql: { sql: "", params: [], duration: 0 }
1387
+ };
1388
+ }
1389
+
1390
+ const snakeTable = snakeCase(table);
1391
+ const query = SqlBuilder.toDeleteInSql({
1392
+ table: snakeTable,
1393
+ idField: "id",
1394
+ ids: ids,
1395
+ quoteIdent: quoteIdentMySql
1396
+ });
1397
+ const executeRes = await this.execute(query.sql, query.params);
1398
+ const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
1399
+ return {
1400
+ data: changes,
1401
+ sql: executeRes.sql
1402
+ };
1403
+ }
1404
+
1405
+ async updBatch(table, dataList) {
1406
+ validateTableBatchDataOptions(table, dataList, "updBatch");
1407
+ if (dataList.length === 0) {
1408
+ return {
1409
+ data: 0,
1410
+ sql: { sql: "", params: [], duration: 0 }
1411
+ };
1412
+ }
1413
+
1414
+ const snakeTable = snakeCase(table);
1415
+ const now = Date.now();
1416
+
1417
+ const processedList = [];
1418
+ const fieldSet = new Set();
1419
+
1420
+ for (const item of dataList) {
1421
+ const userData = this._buildPartialUpdateData({ data: item.data, allowState: true });
1422
+
1423
+ for (const key of Object.keys(userData)) {
1424
+ fieldSet.add(key);
1425
+ }
1426
+
1427
+ processedList.push({ id: item.id, data: userData });
1428
+ }
1429
+
1430
+ const fields = Array.from(fieldSet).sort();
1431
+ if (fields.length === 0) {
1432
+ return {
1433
+ data: 0,
1434
+ sql: { sql: "", params: [], duration: 0 }
1435
+ };
1436
+ }
1437
+
1438
+ const query = SqlBuilder.toUpdateCaseByIdSql({
1439
+ table: snakeTable,
1440
+ idField: "id",
1441
+ rows: processedList,
1442
+ fields: fields,
1443
+ quoteIdent: quoteIdentMySql,
1444
+ updatedAtField: this.beflyMode === "auto" ? "updated_at" : "",
1445
+ updatedAtValue: this.beflyMode === "auto" ? now : null,
1446
+ stateField: "state",
1447
+ stateGtZero: this.beflyMode === "auto"
1448
+ });
1449
+
1450
+ const executeRes = await this.execute(query.sql, query.params);
1451
+ const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
1452
+ return {
1453
+ data: changes,
1454
+ sql: executeRes.sql
1455
+ };
1456
+ }
1457
+
1458
+ async updData(options) {
1459
+ validateTableDataOptions(options, "updData");
1460
+ validateTableWhereOptions(options, "updData", true);
1461
+ const { table, data, where } = options;
1462
+ const prepared = this.prepareRequiredSingleTableWhere(table, where, true, "updData");
1463
+
1464
+ const processed = this._buildUpdateRow({ data: data, now: Date.now(), allowState: true, beflyMode: this.beflyMode });
1465
+ const builder = this.createSqlBuilder().where(prepared.whereFiltered);
1466
+ const { sql, params } = builder.toUpdateSql(prepared.snakeTable, processed);
1467
+
1468
+ const executeRes = await this.execute(sql, params);
1469
+ const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
1470
+ return {
1471
+ data: changes,
1472
+ sql: executeRes.sql
1473
+ };
1474
+ }
1475
+
1476
+ async delData(options) {
1477
+ validateTableWhereOptions(options, "delData", true);
1478
+ const { table, where } = options;
1479
+ const prepared = this.prepareRequiredSingleTableWhere(table, where, true, "delData");
1480
+ const now = Date.now();
1481
+ const processed = {
1482
+ state: 0,
1483
+ deleted_at: now
1484
+ };
1485
+
1486
+ if (this.beflyMode === "auto") {
1487
+ processed.updated_at = now;
1488
+ }
1489
+
1490
+ const builder = this.createSqlBuilder().where(prepared.whereFiltered);
1491
+ const { sql, params } = builder.toUpdateSql(prepared.snakeTable, processed);
1492
+ const executeRes = await this.execute(sql, params);
1493
+ const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
1494
+
1495
+ return {
1496
+ data: changes,
1497
+ sql: executeRes.sql
1498
+ };
1499
+ }
1500
+
1501
+ async delForce(options) {
1502
+ validateTableWhereOptions(options, "delForce", true);
1503
+ const { table, where } = options;
1504
+ const prepared = this.prepareRequiredSingleTableWhere(table, where, false, "delForce");
1505
+
1506
+ const builder = this.createSqlBuilder().where(prepared.whereFiltered);
1507
+ const { sql, params } = builder.toDeleteSql(prepared.snakeTable);
1508
+
1509
+ const executeRes = await this.execute(sql, params);
1510
+ const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
1511
+ return {
1512
+ data: changes,
1513
+ sql: executeRes.sql
1514
+ };
1515
+ }
1516
+
1517
+ async disableData(options) {
1518
+ validateTableWhereOptions(options, "disableData", true);
1519
+ const { table, where } = options;
1520
+
1521
+ return await this.updData({
1522
+ table: table,
1523
+ data: {
1524
+ state: 2
1525
+ },
1526
+ where: where
1527
+ });
1528
+ }
1529
+
1530
+ async enableData(options) {
1531
+ validateTableWhereOptions(options, "enableData", true);
1532
+ const { table, where } = options;
1533
+
1534
+ return await this.updData({
1535
+ table: table,
1536
+ data: {
1537
+ state: 1
1538
+ },
1539
+ where: where
1540
+ });
1541
+ }
1542
+
1543
+ async increment(table, field, where, value = 1) {
1544
+ validateIncrementOptions(table, field, where, value, "increment");
1545
+ const prepared = this.prepareRequiredSingleTableWhere(table, where, true, "increment");
1546
+ const snakeField = snakeCase(field);
1547
+
1548
+ const builder = this.createSqlBuilder().where(prepared.whereFiltered);
1549
+ const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
1550
+
1551
+ const quotedTable = quoteIdentMySql(prepared.snakeTable);
1552
+ const quotedField = quoteIdentMySql(snakeField);
1553
+ const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
1554
+
1555
+ const params = [value];
1556
+ for (const param of whereParams) {
1557
+ params.push(param);
1558
+ }
1559
+
1560
+ const executeRes = await this.execute(sql, params);
1561
+ const changes = normalizeSqlMetaNumber(executeRes.data?.affectedRows);
1562
+ return {
1563
+ data: changes,
1564
+ sql: executeRes.sql
1565
+ };
1566
+ }
1567
+
1568
+ async decrement(table, field, where, value = 1) {
1569
+ return await this.increment(table, field, where, -value);
1570
+ }
1571
+
1572
+ async trans(callback) {
1573
+ const abortMark = "__beflyTransAbort";
1574
+
1575
+ if (this.isTransaction) {
1576
+ const result = await callback(this);
1577
+ if (result?.code !== undefined && result.code !== 0) {
1578
+ const abortError = new Error("TRANSACTION_ABORT", {
1579
+ cause: null,
1580
+ code: "runtime"
1581
+ });
1582
+ abortError[abortMark] = true;
1583
+ abortError.payload = result;
1584
+ throw abortError;
1585
+ }
1586
+ return result;
1587
+ }
1588
+
1589
+ if (typeof this.sql?.begin !== "function") {
1590
+ throw new Error("不支持事务 begin() 方法", {
1591
+ cause: null,
1592
+ code: "runtime"
1593
+ });
1594
+ }
1595
+
1596
+ try {
1597
+ return await this.sql.begin(async (tx) => {
1598
+ const trans = new this.constructor({
1599
+ redis: this.redis,
1600
+ dbName: this.dbName,
1601
+ sql: tx,
1602
+ isTransaction: true,
1603
+ beflyMode: this.beflyMode
1604
+ });
1605
+ const result = await callback(trans);
1606
+ if (result?.code !== undefined && result.code !== 0) {
1607
+ const abortError = new Error("TRANSACTION_ABORT", {
1608
+ cause: null,
1609
+ code: "runtime"
1610
+ });
1611
+ abortError[abortMark] = true;
1612
+ abortError.payload = result;
1613
+ throw abortError;
1614
+ }
1615
+ return result;
1616
+ });
1617
+ } catch (error) {
1618
+ if (error && error[abortMark] === true) {
1619
+ return error.payload;
1620
+ }
1621
+ throw error;
1622
+ }
1623
+ }
1624
+ }
1625
+
1626
+ export { DbHelper };