@maiyunnet/kebab 9.15.0 → 9.15.2
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 +419 -179
- 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/fs.js +5 -2
- 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/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/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.15.
|
|
9
|
+
export const VER = '9.15.2';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/db/conn.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as pg from 'pg';
|
|
|
2
2
|
import * as lCore from '#kebab/lib/core.js';
|
|
3
3
|
import * as lTime from '#kebab/lib/time.js';
|
|
4
4
|
import * as lDb from '#kebab/lib/db.js';
|
|
5
|
+
import * as lSqlValue from '#kebab/lib/sql/value.js';
|
|
5
6
|
// --- 注册解析器 ---
|
|
6
7
|
// --- pg 库对以下类型默认返回 string,为与 MySQL 行为保持一致,在此注册解析器转为 JS 原生类型 ---
|
|
7
8
|
// --- POLYGON: 返回如 ((1,1),(2,2)) 的字符串,解析为 {x, y}[] ---
|
|
@@ -155,6 +156,7 @@ export class Connection {
|
|
|
155
156
|
'result': 1,
|
|
156
157
|
};
|
|
157
158
|
try {
|
|
159
|
+
values = lSqlValue.serializeList(values);
|
|
158
160
|
this.refreshLast();
|
|
159
161
|
if (this._lastSql.length === 2) {
|
|
160
162
|
this._lastSql.splice(0, 1);
|
|
@@ -212,6 +214,7 @@ export class Connection {
|
|
|
212
214
|
'result': 1,
|
|
213
215
|
};
|
|
214
216
|
try {
|
|
217
|
+
values = lSqlValue.serializeList(values);
|
|
215
218
|
this.refreshLast();
|
|
216
219
|
if (this._lastSql.length === 2) {
|
|
217
220
|
this._lastSql.splice(0, 1);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project: Kebab, User: JianSuoQiYue
|
|
3
|
+
* --- 数据库特殊值的运行时包装与序列化 ---
|
|
4
|
+
*/
|
|
5
|
+
import * as kebab from '#kebab/index.js';
|
|
6
|
+
/** --- JSON 数据的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
|
|
7
|
+
declare const jsonValueSymbol: unique symbol;
|
|
8
|
+
/** --- 延迟到数据库边界序列化的 JSON 值 --- */
|
|
9
|
+
export interface IJsonValue<T> {
|
|
10
|
+
readonly [jsonValueSymbol]: true;
|
|
11
|
+
readonly 'value': T;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* --- 创建延迟序列化的 JSON 值 ---
|
|
15
|
+
* @param value 原始 JSON 值
|
|
16
|
+
* @returns JSON 包装值
|
|
17
|
+
*/
|
|
18
|
+
export declare function json<T>(value: T): IJsonValue<T>;
|
|
19
|
+
/**
|
|
20
|
+
* --- 判断是否为 JSON 包装值 ---
|
|
21
|
+
* @param value 待判断的值
|
|
22
|
+
* @returns 是否为 JSON 包装值
|
|
23
|
+
*/
|
|
24
|
+
export declare function isJson(value: unknown): value is IJsonValue<unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
|
|
27
|
+
* @param value 待解包的值
|
|
28
|
+
* @returns 原始值
|
|
29
|
+
*/
|
|
30
|
+
export declare function unwrapJson<T>(value: T): T;
|
|
31
|
+
/**
|
|
32
|
+
* --- 将单个数据库参数转换为驱动可识别的值 ---
|
|
33
|
+
* @param value 数据库参数
|
|
34
|
+
* @returns 序列化后的数据库参数
|
|
35
|
+
*/
|
|
36
|
+
export declare function serialize(value: kebab.Json): kebab.Json;
|
|
37
|
+
/**
|
|
38
|
+
* --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
|
|
39
|
+
* @param values 数据库参数列表
|
|
40
|
+
* @returns 可直接交给数据库驱动的参数列表
|
|
41
|
+
*/
|
|
42
|
+
export declare function serializeList(values?: kebab.DbValue[]): kebab.DbValue[] | undefined;
|
|
43
|
+
export {};
|
package/lib/db/value.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as lText from '#kebab/lib/text.js';
|
|
2
|
+
/** --- JSON 数据的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
|
|
3
|
+
const jsonValueSymbol = Symbol('kebab.db.json');
|
|
4
|
+
/**
|
|
5
|
+
* --- 创建延迟序列化的 JSON 值 ---
|
|
6
|
+
* @param value 原始 JSON 值
|
|
7
|
+
* @returns JSON 包装值
|
|
8
|
+
*/
|
|
9
|
+
export function json(value) {
|
|
10
|
+
return {
|
|
11
|
+
[jsonValueSymbol]: true,
|
|
12
|
+
'value': value,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* --- 判断是否为 JSON 包装值 ---
|
|
17
|
+
* @param value 待判断的值
|
|
18
|
+
* @returns 是否为 JSON 包装值
|
|
19
|
+
*/
|
|
20
|
+
export function isJson(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' &&
|
|
22
|
+
!!value[jsonValueSymbol];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
|
|
26
|
+
* @param value 待解包的值
|
|
27
|
+
* @returns 原始值
|
|
28
|
+
*/
|
|
29
|
+
export function unwrapJson(value) {
|
|
30
|
+
return (isJson(value) ? value.value : value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* --- 将单个数据库参数转换为驱动可识别的值 ---
|
|
34
|
+
* @param value 数据库参数
|
|
35
|
+
* @returns 序列化后的数据库参数
|
|
36
|
+
*/
|
|
37
|
+
export function serialize(value) {
|
|
38
|
+
return isJson(value) ? lText.stringifyJson(value.value) : value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
|
|
42
|
+
* @param values 数据库参数列表
|
|
43
|
+
* @returns 可直接交给数据库驱动的参数列表
|
|
44
|
+
*/
|
|
45
|
+
export function serializeList(values) {
|
|
46
|
+
if (!values) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
let serialized;
|
|
50
|
+
for (let i = 0; i < values.length; ++i) {
|
|
51
|
+
const value = values[i];
|
|
52
|
+
if (!isJson(value)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
serialized ??= [...values];
|
|
56
|
+
serialized[i] = lText.stringifyJson(value.value);
|
|
57
|
+
}
|
|
58
|
+
return serialized ?? values;
|
|
59
|
+
}
|
package/lib/fs.js
CHANGED
|
@@ -434,9 +434,12 @@ export async function readToResponse(path, req, res, stat) {
|
|
|
434
434
|
// --- 判断缓存以及 MIME 和编码 ---
|
|
435
435
|
let charset = '';
|
|
436
436
|
const mimeData = mime.getData(path);
|
|
437
|
-
|
|
437
|
+
// --- 文本类文件统一附加 charset,避免浏览器按本地默认编码解析导致乱码 ---
|
|
438
|
+
if (mimeData.mime.startsWith('text/') || ['json', 'xml', 'svg', 'js', 'mjs', 'map', 'webmanifest'].includes(mimeData.extension)) {
|
|
438
439
|
charset = '; charset=utf-8';
|
|
439
|
-
|
|
440
|
+
}
|
|
441
|
+
// --- 这些文件可能需要缓存 ---
|
|
442
|
+
if (['htm', 'html', 'css', 'js', 'mjs', 'xml', 'jpg', 'jpeg', 'svg', 'gif', 'png', 'json'].includes(mimeData.extension)) {
|
|
440
443
|
const hash = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
|
|
441
444
|
const lastModified = stat.mtime.toUTCString();
|
|
442
445
|
res.setHeader('etag', hash);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project: Kebab, User: JianSuoQiYue
|
|
3
|
+
* --- SQL 特殊参数的运行时包装与序列化 ---
|
|
4
|
+
*/
|
|
5
|
+
import * as kebab from '#kebab/index.js';
|
|
6
|
+
/** --- JSON 参数的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
|
|
7
|
+
declare const jsonValueSymbol: unique symbol;
|
|
8
|
+
/** --- 延迟到数据库边界序列化的 JSON 参数 --- */
|
|
9
|
+
export interface IJsonValue<T> {
|
|
10
|
+
readonly [jsonValueSymbol]: true;
|
|
11
|
+
readonly 'value': T;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* --- 创建延迟序列化的 JSON 参数 ---
|
|
15
|
+
* @param value 原始 JSON 值
|
|
16
|
+
* @returns JSON 包装值
|
|
17
|
+
*/
|
|
18
|
+
export declare function json<T>(value: T): IJsonValue<T>;
|
|
19
|
+
/**
|
|
20
|
+
* --- 判断是否为 JSON 包装值 ---
|
|
21
|
+
* @param value 待判断的值
|
|
22
|
+
* @returns 是否为 JSON 包装值
|
|
23
|
+
*/
|
|
24
|
+
export declare function isJson(value: unknown): value is IJsonValue<unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
|
|
27
|
+
* @param value 待解包的值
|
|
28
|
+
* @returns 原始值
|
|
29
|
+
*/
|
|
30
|
+
export declare function unwrapJson<T>(value: T): T;
|
|
31
|
+
/**
|
|
32
|
+
* --- 将单个 SQL 参数转换为数据库驱动可识别的值 ---
|
|
33
|
+
* @param value SQL 参数
|
|
34
|
+
* @returns 序列化后的数据库参数
|
|
35
|
+
*/
|
|
36
|
+
export declare function serialize(value: kebab.Json): kebab.Json;
|
|
37
|
+
/**
|
|
38
|
+
* --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
|
|
39
|
+
* @param values SQL 参数列表
|
|
40
|
+
* @returns 可直接交给数据库驱动的参数列表
|
|
41
|
+
*/
|
|
42
|
+
export declare function serializeList(values?: kebab.DbValue[]): kebab.DbValue[] | undefined;
|
|
43
|
+
export {};
|
package/lib/sql/value.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as lText from '#kebab/lib/text.js';
|
|
2
|
+
/** --- JSON 参数的内部标记,不污染原始对象,也不会参与 JSON 序列化 --- */
|
|
3
|
+
const jsonValueSymbol = Symbol('kebab.sql.json');
|
|
4
|
+
/**
|
|
5
|
+
* --- 创建延迟序列化的 JSON 参数 ---
|
|
6
|
+
* @param value 原始 JSON 值
|
|
7
|
+
* @returns JSON 包装值
|
|
8
|
+
*/
|
|
9
|
+
export function json(value) {
|
|
10
|
+
return {
|
|
11
|
+
[jsonValueSymbol]: true,
|
|
12
|
+
'value': value,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* --- 判断是否为 JSON 包装值 ---
|
|
17
|
+
* @param value 待判断的值
|
|
18
|
+
* @returns 是否为 JSON 包装值
|
|
19
|
+
*/
|
|
20
|
+
export function isJson(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' &&
|
|
22
|
+
!!value[jsonValueSymbol];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* --- 获取 JSON 包装值内的原始值;普通值保持不变 ---
|
|
26
|
+
* @param value 待解包的值
|
|
27
|
+
* @returns 原始值
|
|
28
|
+
*/
|
|
29
|
+
export function unwrapJson(value) {
|
|
30
|
+
return (isJson(value) ? value.value : value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* --- 将单个 SQL 参数转换为数据库驱动可识别的值 ---
|
|
34
|
+
* @param value SQL 参数
|
|
35
|
+
* @returns 序列化后的数据库参数
|
|
36
|
+
*/
|
|
37
|
+
export function serialize(value) {
|
|
38
|
+
return isJson(value) ? lText.stringifyJson(value.value) : value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* --- 在进入数据库驱动前序列化特殊参数,不修改调用方数组 ---
|
|
42
|
+
* @param values SQL 参数列表
|
|
43
|
+
* @returns 可直接交给数据库驱动的参数列表
|
|
44
|
+
*/
|
|
45
|
+
export function serializeList(values) {
|
|
46
|
+
if (!values) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
let serialized;
|
|
50
|
+
for (let i = 0; i < values.length; ++i) {
|
|
51
|
+
const value = values[i];
|
|
52
|
+
if (!isJson(value)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
serialized ??= [...values];
|
|
56
|
+
serialized[i] = lText.stringifyJson(value.value);
|
|
57
|
+
}
|
|
58
|
+
return serialized ?? values;
|
|
59
|
+
}
|
package/lib/sql.d.ts
CHANGED
|
@@ -98,7 +98,7 @@ export declare class Sql {
|
|
|
98
98
|
/**
|
|
99
99
|
* --- 批量 UPDATE,以子查询作为数据源,纯更新语义(不会插入新行)---
|
|
100
100
|
* --- MySQL: UPDATE t INNER JOIN (SELECT col AS alias ... UNION ALL SELECT ...) AS tmp ON t.key=tmp.key SET t.c=tmp.c ---
|
|
101
|
-
* --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
|
|
101
|
+
* --- PostgreSQL: UPDATE t SET c=tmp.c FROM (VALUES (typed nulls), ($1,...)) AS tmp(cols) WHERE t.key=tmp.key ---
|
|
102
102
|
* @param table 表名
|
|
103
103
|
* @param key 用于定位待更新记录的字段名,通常为主键或唯一键,至少必须建立索引;
|
|
104
104
|
* 该参数是字段名而不是索引名,仅参与 ON / WHERE 匹配,不会被更新
|
|
@@ -192,14 +192,13 @@ export declare class Sql {
|
|
|
192
192
|
private _whereDataPosition;
|
|
193
193
|
/**
|
|
194
194
|
* --- 筛选器 ---
|
|
195
|
-
* ---
|
|
196
|
-
* ---
|
|
197
|
-
* ---
|
|
198
|
-
* ---
|
|
199
|
-
* ---
|
|
200
|
-
* ---
|
|
201
|
-
* ---
|
|
202
|
-
* --- 8. ['info', 'json', {'a': 1}] ---
|
|
195
|
+
* --- 标量相等:'city': 'bj', 'type': '2' ---
|
|
196
|
+
* --- 运算符条件:['type', '>', '1'] ---
|
|
197
|
+
* --- 集合条件:['type', 'in', ['1', '2']] 或 'type': ['1', '2'] ---
|
|
198
|
+
* --- 逻辑分组:'$or': [{'city': 'bj'}, {'city': 'sh'}, [['age', '>', '10']]] ---
|
|
199
|
+
* --- 字段比较:'city_in': column('city_out') ---
|
|
200
|
+
* --- 原始条件:['JSON_CONTAINS(`uid`, ?)', ['hello']] ---
|
|
201
|
+
* --- JSON 条件:['info', 'json', {'a': 1}] ---
|
|
203
202
|
* @param s 筛选数据
|
|
204
203
|
*/
|
|
205
204
|
where(s: string | kebab.Json): this;
|
|
@@ -274,11 +273,6 @@ export declare class Sql {
|
|
|
274
273
|
private _isValue;
|
|
275
274
|
/** --- 获取占位符 --- */
|
|
276
275
|
private _placeholder;
|
|
277
|
-
/**
|
|
278
|
-
* --- 返回 PostgreSQL VALUES 第一行的显式类型转换后缀,用于帮助 PostgreSQL 推断 VALUES 派生表列类型 ---
|
|
279
|
-
* @param v 要处理的值
|
|
280
|
-
*/
|
|
281
|
-
private _pgCastSuffix;
|
|
282
276
|
/**
|
|
283
277
|
* --- 处理单个值,检测数据类型并返回 SQL 和 data ---
|
|
284
278
|
* @param v 要处理的值
|
|
@@ -326,7 +320,8 @@ export declare function value(val: kebab.DbValue): {
|
|
|
326
320
|
'value': kebab.DbValue;
|
|
327
321
|
};
|
|
328
322
|
/**
|
|
329
|
-
* ---
|
|
330
|
-
* @param obj
|
|
323
|
+
* --- 标记需要写入 JSON/jsonb 字段的值;实际序列化延迟到数据库边界 ---
|
|
324
|
+
* @param obj 原始 JSON 值
|
|
325
|
+
* @returns 类型保持不变的 JSON 包装值
|
|
331
326
|
*/
|
|
332
327
|
export declare function json<T>(obj: T): T;
|
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
|
}
|