@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/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
- for (let ri = 0; ri < rows.length; ri++) {
240
- const row = rows[ri];
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
- // --- 第一行加显式类型转换,帮助 PostgreSQL 推断 VALUES 派生表的列类型 ---
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'], // 1, '1' 可能也是 1 数字类型
314
+ ['total', '+', '1'],
313
315
  {
314
- 'type': '6', // 2
315
- 'type': column('type2'), // 3
316
- // 'type': ['type3'], // 4 - 此写法已被禁止,请用 (3) 代替
317
- 'type': ['(CASE `id` WHEN 1 THEN ? WHEN 2 THEN ? END)', ['val1', 'val2']], // 5
318
- 'point': { 'x': 0, 'y': 0 }, // 6
319
- 'polygon': [ [ { 'x': 0, 'y': 0 }, { ... } ], [ ... ] ], // 7
320
- 'json': { 'a': 1, 'b': { 'c': 2 }, 'c': [ { 'c': 2 } ] }, // 8 - 对象类 json,可能为空对象
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
- // --- 1 ---
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]) + ' = ' + this._placeholder() + ', ';
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] + ' ' + this._placeholder() + ', ';
352
+ sql += this.field(v[0]) + ' = ' + this.field(v[0]) + ' ' + v[1] + ' ' + result.sql + ', ';
347
353
  }
348
- this._data.push(nv);
354
+ this._data.push(...result.data);
349
355
  }
350
356
  }
351
357
  else {
352
- // --- 2, 3, 4, 5, 6, 7, 8 ---
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
- * --- 1. 'city': 'bj', 'type': '2' ---
501
- * --- 2. ['type', '>', '1'] ---
502
- * --- 3. ['type', 'in', ['1', '2']] ---
503
- * --- 4. 'type': ['1', '2'] ---
504
- * --- 5. '$or': [{'city': 'bj'}, {'city': 'sh'}, [['age', '>', '10']]], 'type': '2' ---
505
- * --- 6. 'city_in': column('city_out') ---
506
- * --- 7. ['JSON_CONTAINS(`uid`, ?)', ['hello']] ---
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
- // --- 2, 3, 7 ---
554
+ // --- 数组条件或原始 SQL 条件 ---
550
555
  if (v[2] === undefined) {
551
- // --- 7 ---
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
- // --- json ---
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
- // --- 3: null ---
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
- // --- 3 ---
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
- // --- 2, 6 ---
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
- // --- 6. field ---
665
+ // --- 字段比较 ---
661
666
  sql += v0sql + ' ' + v[1] + ' ' + this.field(nv.value) + ' AND ';
662
667
  }
663
668
  else {
664
- sql += v0sql + ' ' + v[1] + ' ' + this._placeholder() + ' AND ';
665
- data.push(nv);
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
- // --- 1, 4, 5, 6 ---
679
+ // --- 字段映射、逻辑分组与 IN 查询 ---
671
680
  if (k.startsWith('$')) {
672
- // --- 5 - '$or': [{'city': 'bj'}, {'city': 'sh'}] ---
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
- // --- 1, 4, 6 ---
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
- // --- 1 ---
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
- // --- 6 ---
714
+ // --- 字段相等 ---
699
715
  sql += this.field(k) + ' = ' + this.field(v.value) + ' AND ';
700
716
  }
701
717
  else {
702
- // --- 4 - 'type': ['1', '2'] ---
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
- * --- 将对象转换为 JSON 字符串并避开类型检查,用于适配 PostgreSQL 的 jsonb 字段 ---
1363
- * @param obj 要转换的 JSON 对象
1343
+ * --- 标记需要写入 JSON/jsonb 字段的值;实际序列化延迟到数据库边界 ---
1344
+ * @param obj 原始 JSON 值
1345
+ * @returns 类型保持不变的 JSON 包装值
1364
1346
  */
1365
1347
  export function json(obj) {
1366
- return lText.stringifyJson(obj);
1348
+ return lSqlValue.json(obj);
1367
1349
  }
@@ -31,7 +31,7 @@ export declare class Response {
31
31
  /**
32
32
  * --- 获取响应读取流对象 ---
33
33
  */
34
- getStream(): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | null;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.15.1",
3
+ "version": "9.15.3",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [
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
- /** --- 创建 JSON 字符串对象,用于 PGSQL 的 jsonb 字段 --- */
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
- /** --- 创建 JSON 字符串对象,用于 PGSQL 的 jsonb 字段 --- */
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 data) {
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
- // --- MySQL 单条 UPDATE 太大时解析、物化、锁持有都会变慢,默认小批量多次执行更稳定;
347
- // PostgreSQL 的 VALUES UPDATE 执行计划较稳定,默认尽量吃满参数上限减少网络往返 ---
348
- const defaultBatchSize = service === lDb.ESERVICE.MYSQL ? 200 : maxBatchSize;
349
- const batchSize = Math.max(1, Math.min(opt.batchSize ?? defaultBatchSize, maxBatchSize));
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._updates[k] = true;
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._updates[n] = true;
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 {
@@ -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', { 'json1': { 'key': 'val', 'key2': 'val2' }, 'json2': [{ 'k1': 'v1' }, { 'k2': 'v2' }], 'json3': { 'x': 1, 'y': 2 }, 'json4': [], 'json5': {} }).where({ 'id': 1 }).getSql();
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', { 'json1': { 'key': 'val', 'key2': 'val2' }, 'json2': [ { 'k1': 'v1' }, { 'k2': 'v2' } ], 'json3': { 'x': 1, 'y': 2 }, 'json4': [], 'json5': {} }).where({ 'id': 1 });</pre>
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)}`);