@maiyunnet/kebab 9.15.1 → 9.15.3
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/doc/kebab-rag.md +454 -184
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/db/conn.js +3 -0
- package/lib/db/value.d.ts +43 -0
- package/lib/db/value.js +59 -0
- package/lib/s3.d.ts +7 -0
- package/lib/s3.js +23 -0
- package/lib/sql/value.d.ts +43 -0
- package/lib/sql/value.js +59 -0
- package/lib/sql.d.ts +11 -16
- package/lib/sql.js +72 -90
- package/lib/undici/response.d.ts +1 -1
- package/package.json +1 -1
- package/sys/mod.d.ts +12 -3
- package/sys/mod.js +56 -16
- package/www/example/ctr/test.js +21 -9
package/lib/sql.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as lText from '#kebab/lib/text.js';
|
|
2
2
|
import * as lCore from '#kebab/lib/core.js';
|
|
3
|
+
import * as lSqlValue from '#kebab/lib/sql/value.js';
|
|
3
4
|
// --- 第三方 ---
|
|
4
5
|
import * as mysql2 from 'mysql2/promise';
|
|
5
6
|
/** --- 服务商定义 --- */
|
|
@@ -198,7 +199,7 @@ export class Sql {
|
|
|
198
199
|
/**
|
|
199
200
|
* --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
|
|
200
201
|
* --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
|
|
201
|
-
* --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
|
|
202
|
+
* --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES (typed nulls), ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
|
|
202
203
|
* @param table 表名
|
|
203
204
|
* @param key 用于定位待更新记录的字段名,通常为主键或唯一键,至少必须建立索引;
|
|
204
205
|
* 该参数是字段名而不是索引名,仅参与 ON / WHERE 匹配,不会被更新
|
|
@@ -234,17 +235,18 @@ export class Sql {
|
|
|
234
235
|
this._sql = [`UPDATE (${selectParts.join(' UNION ALL ')}) AS tmp STRAIGHT_JOIN ${quotedTable} t ON t.${quotedKey} = tmp.${quotedKey} SET ${setClauses}`];
|
|
235
236
|
}
|
|
236
237
|
else {
|
|
237
|
-
// --- PostgreSQL 使用 UPDATE FROM (VALUES ...)
|
|
238
|
+
// --- PostgreSQL 使用 UPDATE FROM (VALUES ...);首行从目标表复合类型取得真实列类型,
|
|
239
|
+
// 避免按 JS 值猜测类型时 uuid、日期、枚举、数组或 NULL 列无法匹配 ---
|
|
238
240
|
const valueParts = [];
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
const typeParts = allCols.map(c => `(NULL::${quotedTable}).${this.field(c)}`);
|
|
242
|
+
valueParts.push(`(${typeParts.join(', ')})`);
|
|
243
|
+
for (const row of rows) {
|
|
241
244
|
const parts = row.map(v => {
|
|
242
245
|
const result = this._processValue(v);
|
|
243
246
|
if (result.data.length > 0) {
|
|
244
247
|
this._data.push(...result.data);
|
|
245
248
|
}
|
|
246
|
-
|
|
247
|
-
return ri === 0 ? result.sql + this._pgCastSuffix(v) : result.sql;
|
|
249
|
+
return result.sql;
|
|
248
250
|
});
|
|
249
251
|
valueParts.push(`(${parts.join(', ')})`);
|
|
250
252
|
}
|
|
@@ -309,16 +311,15 @@ export class Sql {
|
|
|
309
311
|
_updateSub(s) {
|
|
310
312
|
/*
|
|
311
313
|
[
|
|
312
|
-
['total', '+', '1'],
|
|
314
|
+
['total', '+', '1'],
|
|
313
315
|
{
|
|
314
|
-
'type': '6',
|
|
315
|
-
'type': column('type2'),
|
|
316
|
-
|
|
317
|
-
'
|
|
318
|
-
'
|
|
319
|
-
'
|
|
320
|
-
'
|
|
321
|
-
'json2': ['abc'] // 9 - 数组类 json,可能为空数组
|
|
316
|
+
'type': '6',
|
|
317
|
+
'type': column('type2'),
|
|
318
|
+
'type': ['(CASE `id` WHEN 1 THEN ? WHEN 2 THEN ? END)', ['val1', 'val2']],
|
|
319
|
+
'point': { 'x': 0, 'y': 0 },
|
|
320
|
+
'polygon': [ [ { 'x': 0, 'y': 0 }, { ... } ], [ ... ] ],
|
|
321
|
+
'json': json({ 'a': 1, 'b': { 'c': 2 }, 'c': [ { 'c': 2 } ] }),
|
|
322
|
+
'json2': json(['abc'])
|
|
322
323
|
}
|
|
323
324
|
]
|
|
324
325
|
*/
|
|
@@ -327,7 +328,7 @@ export class Sql {
|
|
|
327
328
|
for (const k in s) {
|
|
328
329
|
const v = s[k];
|
|
329
330
|
if (/^[0-9]+$/.test(k)) {
|
|
330
|
-
// ---
|
|
331
|
+
// --- 数组运算式:[字段, 运算符, 值] ---
|
|
331
332
|
const nv = v[2];
|
|
332
333
|
const isf = this._isField(nv);
|
|
333
334
|
if (isf) {
|
|
@@ -339,17 +340,22 @@ export class Sql {
|
|
|
339
340
|
}
|
|
340
341
|
}
|
|
341
342
|
else {
|
|
343
|
+
/** --- JSON 标记值需要生成 ::jsonb;普通运算值保持原占位符行为 --- */
|
|
344
|
+
const result = lSqlValue.isJson(nv) ? this._processValue(nv) : {
|
|
345
|
+
'sql': this._placeholder(),
|
|
346
|
+
'data': [nv],
|
|
347
|
+
};
|
|
342
348
|
if (v[1] === '=') {
|
|
343
|
-
sql += this.field(v[0]) + ' = ' +
|
|
349
|
+
sql += this.field(v[0]) + ' = ' + result.sql + ', ';
|
|
344
350
|
}
|
|
345
351
|
else {
|
|
346
|
-
sql += this.field(v[0]) + ' = ' + this.field(v[0]) + ' ' + v[1] + ' ' +
|
|
352
|
+
sql += this.field(v[0]) + ' = ' + this.field(v[0]) + ' ' + v[1] + ' ' + result.sql + ', ';
|
|
347
353
|
}
|
|
348
|
-
this._data.push(
|
|
354
|
+
this._data.push(...result.data);
|
|
349
355
|
}
|
|
350
356
|
}
|
|
351
357
|
else {
|
|
352
|
-
// ---
|
|
358
|
+
// --- 对象赋值式:{字段: 值} ---
|
|
353
359
|
sql += this.field(k) + ' = ';
|
|
354
360
|
const result = this._processValue(v);
|
|
355
361
|
sql += result.sql + ', ';
|
|
@@ -497,14 +503,13 @@ export class Sql {
|
|
|
497
503
|
_whereDataPosition = [0, 0];
|
|
498
504
|
/**
|
|
499
505
|
* --- 筛选器 ---
|
|
500
|
-
* ---
|
|
501
|
-
* ---
|
|
502
|
-
* ---
|
|
503
|
-
* ---
|
|
504
|
-
* ---
|
|
505
|
-
* ---
|
|
506
|
-
* ---
|
|
507
|
-
* --- 8. ['info', 'json', {'a': 1}] ---
|
|
506
|
+
* --- 标量相等:'city': 'bj', 'type': '2' ---
|
|
507
|
+
* --- 运算符条件:['type', '>', '1'] ---
|
|
508
|
+
* --- 集合条件:['type', 'in', ['1', '2']] 或 'type': ['1', '2'] ---
|
|
509
|
+
* --- 逻辑分组:'$or': [{'city': 'bj'}, {'city': 'sh'}, [['age', '>', '10']]] ---
|
|
510
|
+
* --- 字段比较:'city_in': column('city_out') ---
|
|
511
|
+
* --- 原始条件:['JSON_CONTAINS(`uid`, ?)', ['hello']] ---
|
|
512
|
+
* --- JSON 条件:['info', 'json', {'a': 1}] ---
|
|
508
513
|
* @param s 筛选数据
|
|
509
514
|
*/
|
|
510
515
|
where(s) {
|
|
@@ -546,18 +551,18 @@ export class Sql {
|
|
|
546
551
|
for (const k in s) {
|
|
547
552
|
const v = s[k];
|
|
548
553
|
if (/^[0-9]+$/.test(k)) {
|
|
549
|
-
// ---
|
|
554
|
+
// --- 数组条件或原始 SQL 条件 ---
|
|
550
555
|
if (v[2] === undefined) {
|
|
551
|
-
// ---
|
|
556
|
+
// --- 原始 SQL 条件及参数 ---
|
|
552
557
|
sql += this.field(v[0]) + ' AND ';
|
|
553
558
|
if (v[1] !== undefined) {
|
|
554
559
|
data.push(...v[1]);
|
|
555
560
|
}
|
|
556
561
|
}
|
|
557
562
|
else if (typeof v[1] === 'string' && ['json', 'json_in', 'json_key', 'json_any', 'json_all', 'json_overlaps'].includes(v[1].toLowerCase())) {
|
|
558
|
-
// ---
|
|
563
|
+
// --- JSON 操作符条件 ---
|
|
559
564
|
const op = v[1].toLowerCase();
|
|
560
|
-
const nv = v[2];
|
|
565
|
+
const nv = lSqlValue.unwrapJson(v[2]);
|
|
561
566
|
if (op === 'json') {
|
|
562
567
|
if (this._service === ESERVICE.MYSQL) {
|
|
563
568
|
sql += `JSON_CONTAINS(${this.field(v[0])}, ${this._placeholder()}) AND `;
|
|
@@ -614,7 +619,7 @@ export class Sql {
|
|
|
614
619
|
}
|
|
615
620
|
}
|
|
616
621
|
else if (v[2] === null) {
|
|
617
|
-
// ---
|
|
622
|
+
// --- NULL 比较 ---
|
|
618
623
|
let opera = v[1];
|
|
619
624
|
if (opera === '!=' || opera === '!==' || opera === '<>') {
|
|
620
625
|
opera = 'IS NOT';
|
|
@@ -628,7 +633,7 @@ export class Sql {
|
|
|
628
633
|
sql += this.field(v[0]) + ' ' + opera + ' NULL AND ';
|
|
629
634
|
}
|
|
630
635
|
else if (Array.isArray(v[2])) {
|
|
631
|
-
// ---
|
|
636
|
+
// --- IN 等集合比较 ---
|
|
632
637
|
sql += this.field(v[0]) + ' ' + v[1].toUpperCase() + ' (';
|
|
633
638
|
for (const v1 of v[2]) {
|
|
634
639
|
if (Array.isArray(v1)) {
|
|
@@ -647,7 +652,7 @@ export class Sql {
|
|
|
647
652
|
sql = sql.slice(0, -2) + ') AND ';
|
|
648
653
|
}
|
|
649
654
|
else {
|
|
650
|
-
// ---
|
|
655
|
+
// --- 普通运算符或字段比较 ---
|
|
651
656
|
const nv = v[2];
|
|
652
657
|
// --- v[0] 也可以是 value() 包裹的字面量值,而不一定是字段名 ---
|
|
653
658
|
const isv0 = this._isValue(v[0]);
|
|
@@ -657,19 +662,23 @@ export class Sql {
|
|
|
657
662
|
}
|
|
658
663
|
const isf = this._isField(nv);
|
|
659
664
|
if (isf) {
|
|
660
|
-
// ---
|
|
665
|
+
// --- 字段比较 ---
|
|
661
666
|
sql += v0sql + ' ' + v[1] + ' ' + this.field(nv.value) + ' AND ';
|
|
662
667
|
}
|
|
663
668
|
else {
|
|
664
|
-
|
|
665
|
-
|
|
669
|
+
const result = lSqlValue.isJson(nv) ? this._processValue(nv) : {
|
|
670
|
+
'sql': this._placeholder(),
|
|
671
|
+
'data': [nv],
|
|
672
|
+
};
|
|
673
|
+
sql += v0sql + ' ' + v[1] + ' ' + result.sql + ' AND ';
|
|
674
|
+
data.push(...result.data);
|
|
666
675
|
}
|
|
667
676
|
}
|
|
668
677
|
}
|
|
669
678
|
else {
|
|
670
|
-
// ---
|
|
679
|
+
// --- 字段映射、逻辑分组与 IN 查询 ---
|
|
671
680
|
if (k.startsWith('$')) {
|
|
672
|
-
// ---
|
|
681
|
+
// --- 逻辑条件分组,如 $or ---
|
|
673
682
|
const sp = ' ' + k.slice(1).split('-')[0].toUpperCase() + ' ';
|
|
674
683
|
sql += '(';
|
|
675
684
|
for (let v1 of v) {
|
|
@@ -685,21 +694,28 @@ export class Sql {
|
|
|
685
694
|
sql = sql.slice(0, -sp.length) + ') AND ';
|
|
686
695
|
}
|
|
687
696
|
else {
|
|
688
|
-
// ---
|
|
697
|
+
// --- 单字段条件 ---
|
|
689
698
|
if (v === null) {
|
|
699
|
+
// --- NULL 判断 ---
|
|
690
700
|
sql += this.field(k) + ' IS NULL AND ';
|
|
691
701
|
}
|
|
692
702
|
else if (typeof v === 'string' || typeof v === 'number') {
|
|
693
|
-
// ---
|
|
703
|
+
// --- 标量相等 ---
|
|
694
704
|
sql += this.field(k) + ' = ' + this._placeholder() + ' AND ';
|
|
695
705
|
data.push(v);
|
|
696
706
|
}
|
|
707
|
+
else if (lSqlValue.isJson(v)) {
|
|
708
|
+
// --- JSON 相等 ---
|
|
709
|
+
const result = this._processValue(v);
|
|
710
|
+
sql += this.field(k) + ' = ' + result.sql + ' AND ';
|
|
711
|
+
data.push(...result.data);
|
|
712
|
+
}
|
|
697
713
|
else if (this._isField(v)) {
|
|
698
|
-
// ---
|
|
714
|
+
// --- 字段相等 ---
|
|
699
715
|
sql += this.field(k) + ' = ' + this.field(v.value) + ' AND ';
|
|
700
716
|
}
|
|
701
717
|
else {
|
|
702
|
-
// ---
|
|
718
|
+
// --- IN 查询,如 'type': ['1', '2'] ---
|
|
703
719
|
if (v.length > 0) {
|
|
704
720
|
sql += this.field(k) + ' IN (';
|
|
705
721
|
for (const v1 of v) {
|
|
@@ -935,7 +951,7 @@ export class Sql {
|
|
|
935
951
|
* --- 获取全部 data ---
|
|
936
952
|
*/
|
|
937
953
|
getData() {
|
|
938
|
-
return this._data;
|
|
954
|
+
return lSqlValue.serializeList(this._data) ?? [];
|
|
939
955
|
}
|
|
940
956
|
/**
|
|
941
957
|
* --- 获取定义的 pre ---
|
|
@@ -1096,48 +1112,6 @@ export class Sql {
|
|
|
1096
1112
|
_placeholder() {
|
|
1097
1113
|
return this._service === ESERVICE.MYSQL ? '?' : `$${this._placeholderCounter++}`;
|
|
1098
1114
|
}
|
|
1099
|
-
/**
|
|
1100
|
-
* --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
|
|
1101
|
-
* @param v 要处理的值
|
|
1102
|
-
*/
|
|
1103
|
-
_pgCastSuffix(v) {
|
|
1104
|
-
if (v === null || v === undefined) {
|
|
1105
|
-
return '';
|
|
1106
|
-
}
|
|
1107
|
-
if (typeof v === 'number') {
|
|
1108
|
-
return Number.isInteger(v) ? '::bigint' : '::float8';
|
|
1109
|
-
}
|
|
1110
|
-
if (typeof v === 'boolean') {
|
|
1111
|
-
return '::boolean';
|
|
1112
|
-
}
|
|
1113
|
-
if (v instanceof Buffer) {
|
|
1114
|
-
return '::bytea';
|
|
1115
|
-
}
|
|
1116
|
-
if (Array.isArray(v)) {
|
|
1117
|
-
// --- 函数式语法 ['FUNC(?)', [...]],不加转换 ---
|
|
1118
|
-
if (typeof v[0] === 'string' && v[0].includes('(')) {
|
|
1119
|
-
return '';
|
|
1120
|
-
}
|
|
1121
|
-
// --- POLYGON ---
|
|
1122
|
-
if (v[0]?.y !== undefined) {
|
|
1123
|
-
return '::polygon';
|
|
1124
|
-
}
|
|
1125
|
-
// --- JSON 数组或 PG 原生数组(text[]、int[] 等),
|
|
1126
|
-
// 不加转换,由 pg 驱动与目标列类型决定 ---
|
|
1127
|
-
return '';
|
|
1128
|
-
}
|
|
1129
|
-
if (typeof v === 'object') {
|
|
1130
|
-
// --- POINT ---
|
|
1131
|
-
if (v.y !== undefined) {
|
|
1132
|
-
return '::point';
|
|
1133
|
-
}
|
|
1134
|
-
// --- JSON 对象(用户应通过 sql.json() 包裹为字符串后传入),不加转换 ---
|
|
1135
|
-
return '';
|
|
1136
|
-
}
|
|
1137
|
-
// --- string:保持 unknown 类型,兼容 text/varchar/jsonb 等目标列类型;
|
|
1138
|
-
// 使用 sql.json() 包裹的 jsonb 数据经此路径,unknown 可隐式 cast 到 jsonb ---
|
|
1139
|
-
return '';
|
|
1140
|
-
}
|
|
1141
1115
|
/**
|
|
1142
1116
|
* --- 处理单个值,检测数据类型并返回 SQL 和 data ---
|
|
1143
1117
|
* @param v 要处理的值
|
|
@@ -1151,6 +1125,12 @@ export class Sql {
|
|
|
1151
1125
|
else if (v === null) {
|
|
1152
1126
|
return { 'sql': 'NULL', 'data': [] };
|
|
1153
1127
|
}
|
|
1128
|
+
else if (lSqlValue.isJson(v)) {
|
|
1129
|
+
return {
|
|
1130
|
+
'sql': this._placeholder() + (this._service === ESERVICE.PGSQL ? '::jsonb' : ''),
|
|
1131
|
+
'data': [lSqlValue.serialize(v)]
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1154
1134
|
else if (typeof v === 'string' || typeof v === 'number') {
|
|
1155
1135
|
return { 'sql': this._placeholder(), 'data': [v] };
|
|
1156
1136
|
}
|
|
@@ -1278,6 +1258,7 @@ export function get(opt) {
|
|
|
1278
1258
|
* @param service 服务商,默认 MySQL
|
|
1279
1259
|
*/
|
|
1280
1260
|
export function format(sql, data, service = ESERVICE.MYSQL) {
|
|
1261
|
+
data = lSqlValue.serializeList(data) ?? [];
|
|
1281
1262
|
if (service === ESERVICE.MYSQL) {
|
|
1282
1263
|
return mysql2.format(sql, data);
|
|
1283
1264
|
}
|
|
@@ -1359,9 +1340,10 @@ export function value(val) {
|
|
|
1359
1340
|
};
|
|
1360
1341
|
}
|
|
1361
1342
|
/**
|
|
1362
|
-
* ---
|
|
1363
|
-
* @param obj
|
|
1343
|
+
* --- 标记需要写入 JSON/jsonb 字段的值;实际序列化延迟到数据库边界 ---
|
|
1344
|
+
* @param obj 原始 JSON 值
|
|
1345
|
+
* @returns 类型保持不变的 JSON 包装值
|
|
1364
1346
|
*/
|
|
1365
1347
|
export function json(obj) {
|
|
1366
|
-
return
|
|
1348
|
+
return lSqlValue.json(obj);
|
|
1367
1349
|
}
|
package/lib/undici/response.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare class Response {
|
|
|
31
31
|
/**
|
|
32
32
|
* --- 获取响应读取流对象 ---
|
|
33
33
|
*/
|
|
34
|
-
getStream():
|
|
34
|
+
getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress | null;
|
|
35
35
|
/**
|
|
36
36
|
* --- 获取原生响应读取流对象 ---
|
|
37
37
|
*/
|
package/package.json
CHANGED
package/sys/mod.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export default class Mod {
|
|
|
44
44
|
protected static _$pre?: string;
|
|
45
45
|
/** --- 要 update 的内容 --- */
|
|
46
46
|
protected _updates: Record<string, boolean>;
|
|
47
|
+
/** --- 要在数据库边界序列化的 JSON 字段 --- */
|
|
48
|
+
protected _jsonUpdates: Record<string, boolean>;
|
|
47
49
|
/** --- 模型获取的属性 --- */
|
|
48
50
|
protected _data: Record<string, any>;
|
|
49
51
|
/** --- 当前选择的分表 _ 后缀,多个代表联查 --- */
|
|
@@ -97,7 +99,7 @@ export default class Mod {
|
|
|
97
99
|
'token': string;
|
|
98
100
|
'value': kebab.DbValue;
|
|
99
101
|
};
|
|
100
|
-
/** ---
|
|
102
|
+
/** --- 标记 JSON 字段;模型内保持原始对象,写入数据库时再序列化 --- */
|
|
101
103
|
static json<T>(obj: T): T;
|
|
102
104
|
/**
|
|
103
105
|
* --- 添加一个序列(允许超过 65536 的占位符会被拆分多次执行) ---
|
|
@@ -181,9 +183,10 @@ export default class Mod {
|
|
|
181
183
|
}): lSql.Sql;
|
|
182
184
|
/**
|
|
183
185
|
* --- 批量更新数据 ---
|
|
184
|
-
* @param db
|
|
186
|
+
* @param db 数据库对象;多批次需要整体原子性时应传入 Transaction
|
|
185
187
|
* @param data 数据列表,每个元素必须包含 key 字段,其余字段为要更新的列;
|
|
186
|
-
*
|
|
188
|
+
* 支持稀疏数据(不同元素可以拥有不同的列集合),内部会自动按列集合分组批量执行;
|
|
189
|
+
* 相同 key 会按输入顺序合并,后出现的同名字段覆盖先出现的值
|
|
187
190
|
* @param key 用于定位待更新记录的字段名,通常为主键或唯一键,至少必须建立索引;
|
|
188
191
|
* 该参数是字段名而不是索引名,仅参与 ON / WHERE 条件匹配,不会被更新;
|
|
189
192
|
* data 中每个元素都必须包含此字段,否则该元素会被跳过
|
|
@@ -304,6 +307,12 @@ export default class Mod {
|
|
|
304
307
|
* @param obj 要转换的 kv 数据列表
|
|
305
308
|
*/
|
|
306
309
|
static toArrayByRecord<T extends Mod>(obj: Record<string, T>): Record<string, Record<string, any>>;
|
|
310
|
+
/**
|
|
311
|
+
* --- 设置单个模型属性,并记录是否需要在数据库边界序列化 ---
|
|
312
|
+
* @param key 字段名
|
|
313
|
+
* @param value 字段值
|
|
314
|
+
*/
|
|
315
|
+
private _setProperty;
|
|
307
316
|
set<T extends this, TK extends keyof T>(n: Record<TK, T[TK] | undefined>): void;
|
|
308
317
|
set<T extends this, TK extends keyof T>(n: TK, v: T[TK]): void;
|
|
309
318
|
/**
|
package/sys/mod.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as lSql from '#kebab/lib/sql.js';
|
|
7
7
|
import * as lDb from '#kebab/lib/db.js';
|
|
8
|
+
import * as lSqlValue from '#kebab/lib/sql/value.js';
|
|
8
9
|
import * as lCore from '#kebab/lib/core.js';
|
|
9
10
|
import * as lText from '#kebab/lib/text.js';
|
|
10
11
|
/** --- 条数列表 --- */
|
|
@@ -58,6 +59,8 @@ export default class Mod {
|
|
|
58
59
|
static _$pre;
|
|
59
60
|
/** --- 要 update 的内容 --- */
|
|
60
61
|
_updates = {};
|
|
62
|
+
/** --- 要在数据库边界序列化的 JSON 字段 --- */
|
|
63
|
+
_jsonUpdates = {};
|
|
61
64
|
/** --- 模型获取的属性 --- */
|
|
62
65
|
_data = {};
|
|
63
66
|
/** --- 当前选择的分表 _ 后缀,多个代表联查 --- */
|
|
@@ -127,7 +130,7 @@ export default class Mod {
|
|
|
127
130
|
static value(val) {
|
|
128
131
|
return lSql.value(val);
|
|
129
132
|
}
|
|
130
|
-
/** ---
|
|
133
|
+
/** --- 标记 JSON 字段;模型内保持原始对象,写入数据库时再序列化 --- */
|
|
131
134
|
static json(obj) {
|
|
132
135
|
return lSql.json(obj);
|
|
133
136
|
}
|
|
@@ -308,9 +311,10 @@ export default class Mod {
|
|
|
308
311
|
}
|
|
309
312
|
/**
|
|
310
313
|
* --- 批量更新数据 ---
|
|
311
|
-
* @param db
|
|
314
|
+
* @param db 数据库对象;多批次需要整体原子性时应传入 Transaction
|
|
312
315
|
* @param data 数据列表,每个元素必须包含 key 字段,其余字段为要更新的列;
|
|
313
|
-
*
|
|
316
|
+
* 支持稀疏数据(不同元素可以拥有不同的列集合),内部会自动按列集合分组批量执行;
|
|
317
|
+
* 相同 key 会按输入顺序合并,后出现的同名字段覆盖先出现的值
|
|
314
318
|
* @param key 用于定位待更新记录的字段名,通常为主键或唯一键,至少必须建立索引;
|
|
315
319
|
* 该参数是字段名而不是索引名,仅参与 ON / WHERE 条件匹配,不会被更新;
|
|
316
320
|
* data 中每个元素都必须包含此字段,否则该元素会被跳过
|
|
@@ -320,9 +324,23 @@ export default class Mod {
|
|
|
320
324
|
if (!data.length) {
|
|
321
325
|
return true;
|
|
322
326
|
}
|
|
327
|
+
// --- 先按 key 合并,避免 UPDATE JOIN / UPDATE FROM 一对多匹配时结果不确定 ---
|
|
328
|
+
const items = new Map();
|
|
329
|
+
for (const item of data) {
|
|
330
|
+
if (!Object.hasOwn(item, key) || item[key] === undefined || item[key] === null) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const current = items.get(item[key]);
|
|
334
|
+
if (current) {
|
|
335
|
+
Object.assign(current, item);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
items.set(item[key], { ...item });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
323
341
|
// --- 按列集合分组(处理稀疏数据,保证每组内所有行的列完全一致)---
|
|
324
342
|
const groups = new Map();
|
|
325
|
-
for (const item of
|
|
343
|
+
for (const item of items.values()) {
|
|
326
344
|
const itemCols = Object.keys(item).filter(k => k !== key).sort();
|
|
327
345
|
if (itemCols.length === 0) {
|
|
328
346
|
continue;
|
|
@@ -343,10 +361,14 @@ export default class Mod {
|
|
|
343
361
|
const service = db.getService() ?? lDb.ESERVICE.PGSQL;
|
|
344
362
|
/** --- 每行占位符数量 = key + 所有列,分批避免超出数据库参数上限 --- */
|
|
345
363
|
const maxBatchSize = Math.floor(65000 / allCols.length);
|
|
346
|
-
// ---
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
const
|
|
364
|
+
// --- 控制单条 SQL 的解析、临时数据和持锁规模;PostgreSQL 不应默认吃满协议参数上限 ---
|
|
365
|
+
const defaultBatchSize = service === lDb.ESERVICE.MYSQL ? 200 : 1_000;
|
|
366
|
+
const requestedBatchSize = opt.batchSize;
|
|
367
|
+
const normalizedBatchSize = (requestedBatchSize !== undefined &&
|
|
368
|
+
Number.isSafeInteger(requestedBatchSize) &&
|
|
369
|
+
requestedBatchSize > 0) ?
|
|
370
|
+
requestedBatchSize : defaultBatchSize;
|
|
371
|
+
const batchSize = Math.max(1, Math.min(normalizedBatchSize, maxBatchSize));
|
|
350
372
|
for (let i = 0; i < groupItems.length; i += batchSize) {
|
|
351
373
|
const batch = groupItems.slice(i, i + batchSize);
|
|
352
374
|
const sq = lSql.get({
|
|
@@ -528,6 +550,25 @@ export default class Mod {
|
|
|
528
550
|
}
|
|
529
551
|
return rtn;
|
|
530
552
|
}
|
|
553
|
+
// --- 动态方法 ---
|
|
554
|
+
/**
|
|
555
|
+
* --- 设置单个模型属性,并记录是否需要在数据库边界序列化 ---
|
|
556
|
+
* @param key 字段名
|
|
557
|
+
* @param value 字段值
|
|
558
|
+
*/
|
|
559
|
+
_setProperty(key, value) {
|
|
560
|
+
const isJson = lSqlValue.isJson(value);
|
|
561
|
+
const rawValue = isJson ? lSqlValue.unwrapJson(value) : value;
|
|
562
|
+
this._updates[key] = true;
|
|
563
|
+
if (isJson) {
|
|
564
|
+
this._jsonUpdates[key] = true;
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
delete this._jsonUpdates[key];
|
|
568
|
+
}
|
|
569
|
+
this._data[key] = rawValue;
|
|
570
|
+
this[key] = rawValue;
|
|
571
|
+
}
|
|
531
572
|
/**
|
|
532
573
|
* --- 设置一个/多个属性,值为 undefined 则不会被更新 ---
|
|
533
574
|
* @param n 字符串或键/值
|
|
@@ -542,9 +583,7 @@ export default class Mod {
|
|
|
542
583
|
continue;
|
|
543
584
|
}
|
|
544
585
|
// --- 强制更新,因为有的可能就是要强制更新既然设置了 ---
|
|
545
|
-
this.
|
|
546
|
-
this._data[k] = v;
|
|
547
|
-
this[k] = v;
|
|
586
|
+
this._setProperty(k, v);
|
|
548
587
|
}
|
|
549
588
|
}
|
|
550
589
|
else {
|
|
@@ -555,9 +594,7 @@ export default class Mod {
|
|
|
555
594
|
if (typeof n !== 'string') {
|
|
556
595
|
return;
|
|
557
596
|
}
|
|
558
|
-
this.
|
|
559
|
-
this._data[n] = v;
|
|
560
|
-
this[n] = v;
|
|
597
|
+
this._setProperty(n, v);
|
|
561
598
|
}
|
|
562
599
|
}
|
|
563
600
|
/**
|
|
@@ -575,7 +612,7 @@ export default class Mod {
|
|
|
575
612
|
const cstr = this.constructor;
|
|
576
613
|
const updates = {};
|
|
577
614
|
for (const k in this._updates) {
|
|
578
|
-
updates[k] = this._data[k];
|
|
615
|
+
updates[k] = this._jsonUpdates[k] ? lSql.json(this._data[k]) : this._data[k];
|
|
579
616
|
}
|
|
580
617
|
let r = null;
|
|
581
618
|
if ((cstr._$key !== '') && (updates[cstr._$key] === undefined)) {
|
|
@@ -647,6 +684,7 @@ export default class Mod {
|
|
|
647
684
|
}
|
|
648
685
|
if (r.packet?.affected) {
|
|
649
686
|
this._updates = {};
|
|
687
|
+
this._jsonUpdates = {};
|
|
650
688
|
this._data[cstr._$primary] = r.packet.insert;
|
|
651
689
|
this[cstr._$primary] = this._data[cstr._$primary];
|
|
652
690
|
return true;
|
|
@@ -681,6 +719,7 @@ export default class Mod {
|
|
|
681
719
|
}
|
|
682
720
|
where[field] = this._data[field];
|
|
683
721
|
delete this._updates[field];
|
|
722
|
+
delete this._jsonUpdates[field];
|
|
684
723
|
}
|
|
685
724
|
return this.save(where);
|
|
686
725
|
}
|
|
@@ -730,7 +769,7 @@ export default class Mod {
|
|
|
730
769
|
}
|
|
731
770
|
const updates = {};
|
|
732
771
|
for (const k in this._updates) {
|
|
733
|
-
updates[k] = this._data[k];
|
|
772
|
+
updates[k] = this._jsonUpdates[k] ? lSql.json(this._data[k]) : this._data[k];
|
|
734
773
|
}
|
|
735
774
|
this._sql.update(cstr._$table + (this._index ? ('_' + this._index[0]) : ''), [updates]).where(where ?? {
|
|
736
775
|
[cstr._$primary]: this._data[cstr._$primary]
|
|
@@ -742,6 +781,7 @@ export default class Mod {
|
|
|
742
781
|
}
|
|
743
782
|
if (r.packet.affected) {
|
|
744
783
|
this._updates = {};
|
|
784
|
+
this._jsonUpdates = {};
|
|
745
785
|
return true;
|
|
746
786
|
}
|
|
747
787
|
else {
|
package/www/example/ctr/test.js
CHANGED
|
@@ -687,7 +687,7 @@ Result:<pre id="result">Nothing.</pre>` + this._getEnd();
|
|
|
687
687
|
{ 'x': 3, 'y': 3 },
|
|
688
688
|
{ 'x': 1, 'y': 1 },
|
|
689
689
|
],
|
|
690
|
-
'json': { 'x': { 'y': 'abc' } },
|
|
690
|
+
'json': mTest.json({ 'x': { 'y': 'abc' } }),
|
|
691
691
|
'time_add': time,
|
|
692
692
|
});
|
|
693
693
|
const result = await test.create();
|
|
@@ -704,7 +704,7 @@ test.set({
|
|
|
704
704
|
{ 'x': 3, 'y': 3 },
|
|
705
705
|
{ 'x': 1, 'y': 1 },
|
|
706
706
|
],
|
|
707
|
-
'json': { 'x': { 'y': 'abc' } },
|
|
707
|
+
'json': mTest.json({ 'x': { 'y': 'abc' } }),
|
|
708
708
|
'time_add': time,
|
|
709
709
|
});
|
|
710
710
|
const result = await test.create();
|
|
@@ -800,7 +800,7 @@ await ft.save();</pre>`);
|
|
|
800
800
|
{ 'x': 7, 'y': 3 },
|
|
801
801
|
{ 'x': 5, 'y': 1 }
|
|
802
802
|
],
|
|
803
|
-
'json': { 'x': { 'y': 'def' } }
|
|
803
|
+
'json': mTest.json({ 'x': { 'y': 'def' } })
|
|
804
804
|
});
|
|
805
805
|
await ft.save();
|
|
806
806
|
await ft.refresh();
|
|
@@ -815,7 +815,7 @@ await ft.save();</pre>`);
|
|
|
815
815
|
{ 'x': 7, 'y': 3 },
|
|
816
816
|
{ 'x': 5, 'y': 1 }
|
|
817
817
|
],
|
|
818
|
-
'json': { 'x': { 'y': 'def' } }
|
|
818
|
+
'json': mTest.json({ 'x': { 'y': 'def' } })
|
|
819
819
|
});
|
|
820
820
|
await ft.save();
|
|
821
821
|
await ft.refresh();</pre>`);
|
|
@@ -2672,7 +2672,7 @@ Result:<pre id="result">Nothing.</pre>`);
|
|
|
2672
2672
|
{ 'x': 3, 'y': 3 },
|
|
2673
2673
|
{ 'x': 1, 'y': 1 }
|
|
2674
2674
|
],
|
|
2675
|
-
{ 'x': { 'y': 'ghi' } }
|
|
2675
|
+
lSql.json({ 'x': { 'y': 'ghi' } })
|
|
2676
2676
|
],
|
|
2677
2677
|
[
|
|
2678
2678
|
'POINT B', ['ST_POINTFROMTEXT(?)', ['POINT(123.147775 30.625016)']], { 'x': 1, 'y': 1 }, null, null
|
|
@@ -2687,7 +2687,7 @@ Result:<pre id="result">Nothing.</pre>`);
|
|
|
2687
2687
|
{ 'x': 3, 'y': 3 },
|
|
2688
2688
|
{ 'x': 1, 'y': 1 }
|
|
2689
2689
|
],
|
|
2690
|
-
{ 'x': { 'y': 'ghi' } }
|
|
2690
|
+
lSql.json({ 'x': { 'y': 'ghi' } })
|
|
2691
2691
|
],
|
|
2692
2692
|
[
|
|
2693
2693
|
'POINT B', ['ST_POINTFROMTEXT(?)', ['POINT(123.147775 30.625016)']], { 'x': 1, 'y': 1 }, null, null
|
|
@@ -2794,10 +2794,22 @@ Result:<pre id="result">Nothing.</pre>`);
|
|
|
2794
2794
|
<b>getSql() :</b> ${s}<br>
|
|
2795
2795
|
<b>getData():</b> <pre>${JSON.stringify(sd, undefined, 4)}</pre>
|
|
2796
2796
|
<b>format() :</b> ${sql.format(s, sd)}`);
|
|
2797
|
-
// --- json ---
|
|
2798
|
-
s = sql.update('json', {
|
|
2797
|
+
// --- JSON/jsonb 字段使用 json() 显式标记 ---
|
|
2798
|
+
s = sql.update('json', {
|
|
2799
|
+
'json1': lSql.json({ 'key': 'val', 'key2': 'val2' }),
|
|
2800
|
+
'json2': lSql.json([{ 'k1': 'v1' }, { 'k2': 'v2' }]),
|
|
2801
|
+
'json3': lSql.json({ 'x': 1, 'y': 2 }),
|
|
2802
|
+
'json4': lSql.json([]),
|
|
2803
|
+
'json5': lSql.json({})
|
|
2804
|
+
}).where({ 'id': 1 }).getSql();
|
|
2799
2805
|
sd = sql.getData();
|
|
2800
|
-
echo.push(`<pre>sql.update('json', {
|
|
2806
|
+
echo.push(`<pre>sql.update('json', {
|
|
2807
|
+
'json1': lSql.json({ 'key': 'val', 'key2': 'val2' }),
|
|
2808
|
+
'json2': lSql.json([ { 'k1': 'v1' }, { 'k2': 'v2' } ]),
|
|
2809
|
+
'json3': lSql.json({ 'x': 1, 'y': 2 }),
|
|
2810
|
+
'json4': lSql.json([]),
|
|
2811
|
+
'json5': lSql.json({})
|
|
2812
|
+
}).where({ 'id': 1 });</pre>
|
|
2801
2813
|
<b>getSql() :</b> ${s}<br>
|
|
2802
2814
|
<b>getData():</b> <pre>${JSON.stringify(sd, undefined, 4)}</pre>
|
|
2803
2815
|
<b>format() :</b> ${sql.format(s, sd)}`);
|