@dtdyq/restbase 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/query.ts ADDED
@@ -0,0 +1,580 @@
1
+ /**
2
+ * query.ts — 查询条件 → SQL
3
+ *
4
+ * 两种入口:
5
+ * A) URL 查询参数(GET/DELETE,用于命令行调试)
6
+ * buildListSQL / buildDeleteSQL
7
+ * B) JSON Body(POST /api/query、POST /api/delete,用于前端)
8
+ * buildBodyListSQL / buildBodyDeleteSQL
9
+ *
10
+ * URL 语法:
11
+ * 普通条件: ?field=value 或 ?field=op.value
12
+ * 操作符: eq ne ge gt le lt is nis(ns) like nlike in nin
13
+ * 逻辑组合: ?or=f.op.v,f2.op.v2 → (cond OR cond)
14
+ * 特殊参数: select / order / pageNo / pageSize / group
15
+ *
16
+ * Body 语法:
17
+ * where: 数组/元组/对象,支持嵌套 and/or
18
+ * select: 字符串或对象数组
19
+ * order: 字符串或对象数组
20
+ * group: 字段名数组
21
+ */
22
+ import type {ColMeta, TblMeta} from "./db.ts";
23
+ import {
24
+ AppError, ownerCond, q,
25
+ type BodyWhereItem, type BodyWhereInput, type BodySelectItem,
26
+ type BodyOrderItem, type BodyQuery,
27
+ } from "./types.ts";
28
+
29
+ /* ─── 操作符映射 ─── */
30
+ const OP: Record<string, string> = {
31
+ eq: "=", ne: "!=", ge: ">=", gt: ">", le: "<=", lt: "<",
32
+ };
33
+ const ALL_OPS = new Set([
34
+ ...Object.keys(OP), "is", "nis", "ns", "like", "nlike", "in", "nin",
35
+ ]);
36
+ const AGG_FUNCS = new Set(["avg", "max", "min", "count", "sum"]);
37
+ const RESERVED = new Set([
38
+ "select", "order", "pageNo", "pageSize", "group", "or", "and",
39
+ ]);
40
+
41
+ /* ─── 输出结构 ─── */
42
+ export interface ListSQL {
43
+ sql: string;
44
+ countSql: string;
45
+ values: unknown[];
46
+ countValues: unknown[];
47
+ pageNo?: number;
48
+ pageSize?: number;
49
+ }
50
+
51
+ export interface DeleteSQL {
52
+ sql: string;
53
+ values: unknown[];
54
+ }
55
+
56
+ /* ─── 内部类型 ─── */
57
+ interface Ctx {
58
+ n: number
59
+ }
60
+
61
+ interface Cond {
62
+ sql: string;
63
+ values: unknown[]
64
+ }
65
+
66
+ /* ══════════════════════════════════════════════════════════════
67
+ 公共:从 URL query 构建 WHERE 子句 + 参数值
68
+ ══════════════════════════════════════════════════════════════ */
69
+
70
+ function buildWhere(
71
+ tbl: TblMeta,
72
+ params: Record<string, string>,
73
+ userId: number,
74
+ ctx: Ctx,
75
+ ): { where: string; values: unknown[] } {
76
+ const wp: string[] = [];
77
+ const wv: unknown[] = [];
78
+
79
+ /* 1) owner 过滤 */
80
+ if (tbl.hasOwner) {
81
+ wp.push(ownerCond(ctx.n++));
82
+ wv.push(userId);
83
+ }
84
+
85
+ /* 2) 普通字段条件(多个字段之间 AND) */
86
+ for (const [key, val] of Object.entries(params)) {
87
+ if (RESERVED.has(key) || !tbl.colMap.has(key)) continue;
88
+ const c = fieldCond(key, val, tbl, ctx);
89
+ wp.push(c.sql);
90
+ wv.push(...c.values);
91
+ }
92
+
93
+ /* 3) or=… / and=… 逻辑组合 */
94
+ if (params.or) {
95
+ const c = group("OR", params.or, tbl, ctx);
96
+ wp.push(c.sql);
97
+ wv.push(...c.values);
98
+ }
99
+ if (params.and) {
100
+ const c = group("AND", params.and, tbl, ctx);
101
+ wp.push(c.sql);
102
+ wv.push(...c.values);
103
+ }
104
+
105
+ const where = wp.length ? `WHERE ${wp.join(" AND ")}` : "";
106
+ return {where, values: wv};
107
+ }
108
+
109
+ /* ══════════════════════════════════════════════════════════════
110
+ 主入口:从 URL query 构建完整 SELECT SQL + COUNT SQL
111
+ ══════════════════════════════════════════════════════════════ */
112
+
113
+ export function buildListSQL(
114
+ tbl: TblMeta,
115
+ params: Record<string, string>,
116
+ userId: number,
117
+ ): ListSQL {
118
+ const ctx: Ctx = {n: 1};
119
+ const {where, values: wv} = buildWhere(tbl, params, userId, ctx);
120
+
121
+ /* SELECT */
122
+ const sel = buildSel(params.select, tbl);
123
+
124
+ /* GROUP BY */
125
+ const grp = params.group
126
+ ? `GROUP BY ${params.group.split(",").map((s) => q(s.trim())).join(", ")}`
127
+ : "";
128
+
129
+ /* ORDER BY */
130
+ const ord = params.order ? `ORDER BY ${buildOrd(params.order)}` : "";
131
+
132
+ /* 分页(pageSize 上限 1000) */
133
+ const pageNo = params.pageNo ? Math.max(1, Number(params.pageNo)) : undefined;
134
+ const pageSize = params.pageSize ? Math.min(1000, Math.max(1, Number(params.pageSize))) : undefined;
135
+ const lim =
136
+ pageNo !== undefined && pageSize !== undefined
137
+ ? `LIMIT ${pageSize} OFFSET ${(pageNo - 1) * pageSize}`
138
+ : "";
139
+
140
+ /* 拼接 */
141
+ const t = q(tbl.name);
142
+ const sql = [`SELECT ${sel} FROM ${t}`, where, grp, ord, lim]
143
+ .filter(Boolean)
144
+ .join(" ");
145
+
146
+ /* COUNT SQL(有 GROUP BY 时用子查询) */
147
+ const countSql = grp
148
+ ? `SELECT COUNT(*) AS total FROM (SELECT 1 FROM ${t} ${where} ${grp}) AS _sub`
149
+ : `SELECT COUNT(*) AS total FROM ${t} ${where}`;
150
+
151
+ return {
152
+ sql,
153
+ countSql,
154
+ values: [...wv],
155
+ countValues: [...wv],
156
+ pageNo,
157
+ pageSize,
158
+ };
159
+ }
160
+
161
+ /* ══════════════════════════════════════════════════════════════
162
+ DELETE SQL:复用 WHERE 构建,支持与 GET 相同的条件语法
163
+ ══════════════════════════════════════════════════════════════ */
164
+
165
+ export function buildDeleteSQL(
166
+ tbl: TblMeta,
167
+ params: Record<string, string>,
168
+ userId: number,
169
+ ): DeleteSQL {
170
+ const ctx: Ctx = {n: 1};
171
+ const {where, values} = buildWhere(tbl, params, userId, ctx);
172
+ const sql = `DELETE
173
+ FROM ${q(tbl.name)} ${where}`;
174
+ return {sql, values: [...values]};
175
+ }
176
+
177
+ /* ══════════════════════════════════════════════════════════════
178
+ 单字段条件: field=op.value 或 field=value(默认 eq)
179
+ ══════════════════════════════════════════════════════════════ */
180
+
181
+ function fieldCond(field: string, raw: string, tbl: TblMeta, ctx: Ctx): Cond {
182
+ const col = tbl.colMap.get(field)!;
183
+
184
+ /* in/nin 无 dot 写法: in(…) nin(…) */
185
+ if (/^n?in\(/.test(raw)) {
186
+ const neg = raw.startsWith("n");
187
+ return buildCond(field, neg ? "nin" : "in", raw.slice(neg ? 3 : 2), col, ctx);
188
+ }
189
+
190
+ /* 标准: op.value */
191
+ const dot = raw.indexOf(".");
192
+ if (dot === -1) return buildCond(field, "eq", raw, col, ctx);
193
+
194
+ const maybeOp = raw.slice(0, dot).toLowerCase();
195
+ if (ALL_OPS.has(maybeOp))
196
+ return buildCond(field, maybeOp, raw.slice(dot + 1), col, ctx);
197
+
198
+ /* 不是已知操作符 → 整个值当 eq */
199
+ return buildCond(field, "eq", raw, col, ctx);
200
+ }
201
+
202
+ /* ══════════════════════════════════════════════════════════════
203
+ 构建单条件 SQL
204
+ ══════════════════════════════════════════════════════════════ */
205
+
206
+ function buildCond(
207
+ field: string, op: string, val: string, col: ColMeta, ctx: Ctx,
208
+ ): Cond {
209
+ const f = q(field);
210
+
211
+ /* IS NULL / IS NOT NULL */
212
+ if (op === "is") return {sql: `${f} IS NULL`, values: []};
213
+ if (op === "nis" || op === "ns") return {sql: `${f} IS NOT NULL`, values: []};
214
+
215
+ /* LIKE / NOT LIKE (* → %) */
216
+ if (op === "like" || op === "nlike") {
217
+ const sqlOp = op === "like" ? "LIKE" : "NOT LIKE";
218
+ return {sql: `${f} ${sqlOp} $${ctx.n++}`, values: [val.replace(/\*/g, "%")]};
219
+ }
220
+
221
+ /* IN / NOT IN / BETWEEN */
222
+ if (op === "in" || op === "nin") return buildIn(f, op === "nin", val, col, ctx);
223
+
224
+ /* 标准比较 eq ne ge gt le lt */
225
+ const sqlOp = OP[op];
226
+ if (!sqlOp) throw new AppError("QUERY_ERROR", `Unknown operator: ${op}`);
227
+ return {sql: `${f} ${sqlOp} $${ctx.n++}`, values: [typed(val, col)]};
228
+ }
229
+
230
+ /* ─── IN / NOT IN / BETWEEN ─── */
231
+
232
+ function buildIn(
233
+ f: string, neg: boolean, val: string, col: ColMeta, ctx: Ctx,
234
+ ): Cond {
235
+ let inner = val;
236
+ if (inner.startsWith("(") && inner.endsWith(")")) inner = inner.slice(1, -1);
237
+ const not = neg ? "NOT " : "";
238
+
239
+ /* BETWEEN: 12...20 */
240
+ if (inner.includes("...")) {
241
+ const [lo, hi] = inner.split("...");
242
+ return {
243
+ sql: `${f} ${not}BETWEEN $${ctx.n++} AND $${ctx.n++}`,
244
+ values: [typed(lo!, col), typed(hi!, col)],
245
+ };
246
+ }
247
+
248
+ /* IN (a, b, c) */
249
+ const items = inner.split(",").map((s) => s.trim());
250
+ const ph = items.map(() => `$${ctx.n++}`).join(", ");
251
+ return {sql: `${f} ${not}IN (${ph})`, values: items.map((s) => typed(s, col))};
252
+ }
253
+
254
+ /* ─── 按列类型转换 ─── */
255
+ function typed(v: string, col: ColMeta): string | number {
256
+ return col.isNumeric ? Number(v) : v;
257
+ }
258
+
259
+ /* ══════════════════════════════════════════════════════════════
260
+ 逻辑分组: or=… / and=… 支持嵌套
261
+ ══════════════════════════════════════════════════════════════ */
262
+
263
+ function group(logic: "AND" | "OR", raw: string, tbl: TblMeta, ctx: Ctx): Cond {
264
+ const parts = splitTop(raw);
265
+ const conds = parts.map((p) => expr(p, tbl, ctx));
266
+ return {
267
+ sql: `(${conds.map((c) => c.sql).join(` ${logic} `)})`,
268
+ values: conds.flatMap((c) => c.values),
269
+ };
270
+ }
271
+
272
+ /** 解析嵌套表达式(递归) */
273
+ function expr(e: string, tbl: TblMeta, ctx: Ctx): Cond {
274
+ /* 嵌套组: or.( … ) / and.( … ) */
275
+ const m = e.match(/^(or|and)\.\((.+)\)$/);
276
+ if (m) return group(m[1]!.toUpperCase() as "AND" | "OR", m[2]!, tbl, ctx);
277
+
278
+ /* 简单条件: field.op.value */
279
+ const dot = e.indexOf(".");
280
+ if (dot === -1) throw new AppError("QUERY_ERROR", `Invalid expression: ${e}`);
281
+ const field = e.slice(0, dot);
282
+ if (!tbl.colMap.has(field))
283
+ throw new AppError("QUERY_ERROR", `Unknown column: ${field}`);
284
+ return fieldCond(field, e.slice(dot + 1), tbl, ctx);
285
+ }
286
+
287
+ /** 逗号分割(尊重括号嵌套) */
288
+ function splitTop(s: string): string[] {
289
+ const r: string[] = [];
290
+ let depth = 0;
291
+ let start = 0;
292
+ for (let i = 0; i < s.length; i++) {
293
+ if (s[i] === "(") depth++;
294
+ else if (s[i] === ")") depth--;
295
+ else if (s[i] === "," && depth === 0) {
296
+ r.push(s.slice(start, i));
297
+ start = i + 1;
298
+ }
299
+ }
300
+ r.push(s.slice(start));
301
+ return r;
302
+ }
303
+
304
+ /* ══════════════════════════════════════════════════════════════
305
+ SELECT 解析: name | age:userAge | max:age | max:age:maxAge
306
+ ══════════════════════════════════════════════════════════════ */
307
+
308
+ function buildSel(raw: string | undefined, _tbl: TblMeta): string {
309
+ if (!raw) return "*";
310
+ return raw
311
+ .split(",")
312
+ .map((item) => {
313
+ const p = item.trim().split(":");
314
+ if (p.length === 1) return q(p[0]!);
315
+ if (p.length === 2) {
316
+ /* func:field 或 field:alias */
317
+ if (AGG_FUNCS.has(p[0]!.toLowerCase())) {
318
+ const fn = p[0]!.toUpperCase();
319
+ return `${fn}(${q(p[1]!)}) AS ${q(`${p[0]!.toLowerCase()}:${p[1]!}`)}`;
320
+ }
321
+ return `${q(p[0]!)} AS ${q(p[1]!)}`;
322
+ }
323
+ /* func:field:alias */
324
+ return `${p[0]!.toUpperCase()}(${q(p[1]!)}) AS ${q(p[2]!)}`;
325
+ })
326
+ .join(", ");
327
+ }
328
+
329
+ /* ══════════════════════════════════════════════════════════════
330
+ ORDER BY 解析: asc.age,desc.name (省略方向默认 ASC)
331
+ ══════════════════════════════════════════════════════════════ */
332
+
333
+ function buildOrd(raw: string): string {
334
+ return raw
335
+ .split(",")
336
+ .map((s) => {
337
+ const t = s.trim();
338
+ if (t.startsWith("asc.")) return `${q(t.slice(4))} ASC`;
339
+ if (t.startsWith("desc.")) return `${q(t.slice(5))} DESC`;
340
+ return `${q(t)} ASC`;
341
+ })
342
+ .join(", ");
343
+ }
344
+
345
+ /* ══════════════════════════════════════════════════════════════════════════
346
+ ██████ Body 模式(POST JSON)→ SQL
347
+ (类型定义和 Zod Schema 见 types.ts)
348
+ ══════════════════════════════════════════════════════════════════════════ */
349
+
350
+ /* ══════════════════════════════════════════════════════════════
351
+ Body 模式入口: 完整 SELECT
352
+ ══════════════════════════════════════════════════════════════ */
353
+
354
+ export function buildBodyListSQL(
355
+ tbl: TblMeta, body: BodyQuery, userId: number,
356
+ ): ListSQL {
357
+ const ctx: Ctx = {n: 1};
358
+ const {where, values: wv} = bodyBuildWhere(tbl, body.where, userId, ctx);
359
+
360
+ const sel = bodyBuildSel(body.select);
361
+ const grp = body.group?.length
362
+ ? `GROUP BY ${body.group.map((f) => q(f)).join(", ")}`
363
+ : "";
364
+ const ord = body.order?.length
365
+ ? `ORDER BY ${bodyBuildOrd(body.order)}`
366
+ : "";
367
+
368
+ const pageNo = body.pageNo ? Math.max(1, body.pageNo) : undefined;
369
+ const pageSize = body.pageSize ? Math.min(1000, Math.max(1, body.pageSize)) : undefined;
370
+ const lim =
371
+ pageNo !== undefined && pageSize !== undefined
372
+ ? `LIMIT ${pageSize} OFFSET ${(pageNo - 1) * pageSize}`
373
+ : "";
374
+
375
+ const t = q(tbl.name);
376
+ const sql = [`SELECT ${sel} FROM ${t}`, where, grp, ord, lim]
377
+ .filter(Boolean)
378
+ .join(" ");
379
+
380
+ const countSql = grp
381
+ ? `SELECT COUNT(*) AS total FROM (SELECT 1 FROM ${t} ${where} ${grp}) AS _sub`
382
+ : `SELECT COUNT(*) AS total FROM ${t} ${where}`;
383
+
384
+ return {
385
+ sql, countSql,
386
+ values: [...wv], countValues: [...wv],
387
+ pageNo, pageSize,
388
+ };
389
+ }
390
+
391
+ /* ══════════════════════════════════════════════════════════════
392
+ Body 模式入口: DELETE
393
+ ══════════════════════════════════════════════════════════════ */
394
+
395
+ export function buildBodyDeleteSQL(
396
+ tbl: TblMeta, where: BodyWhereInput | undefined, userId: number,
397
+ ): DeleteSQL {
398
+ const ctx: Ctx = {n: 1};
399
+ const w = bodyBuildWhere(tbl, where, userId, ctx);
400
+ return {
401
+ sql: `DELETE
402
+ FROM ${q(tbl.name)} ${w.where}`, values: [...w.values]
403
+ };
404
+ }
405
+
406
+ /* ══════════════════════════════════════════════════════════════
407
+ Body WHERE → SQL
408
+ ══════════════════════════════════════════════════════════════ */
409
+
410
+ function bodyBuildWhere(
411
+ tbl: TblMeta, input: BodyWhereInput | undefined, userId: number, ctx: Ctx,
412
+ ): { where: string; values: unknown[] } {
413
+ const wp: string[] = [];
414
+ const wv: unknown[] = [];
415
+
416
+ /* owner 过滤 */
417
+ if (tbl.hasOwner) {
418
+ wp.push(ownerCond(ctx.n++));
419
+ wv.push(userId);
420
+ }
421
+
422
+ if (input) {
423
+ const items = normalizeWhereInput(input);
424
+ for (const item of items) {
425
+ const c = bodyCondItem(item, tbl, ctx);
426
+ wp.push(c.sql);
427
+ wv.push(...c.values);
428
+ }
429
+ }
430
+
431
+ const where = wp.length ? `WHERE ${wp.join(" AND ")}` : "";
432
+ return {where, values: wv};
433
+ }
434
+
435
+ /**
436
+ * 规范化 where 输入:
437
+ * - 如果是数组且首元素为 string → 当作单条件元组
438
+ * - 如果是对象(非数组)→ 包装为单元素数组
439
+ * - 否则 → 条件数组
440
+ */
441
+ function normalizeWhereInput(input: BodyWhereInput): BodyWhereItem[] {
442
+ if (!Array.isArray(input)) return [input as BodyWhereItem];
443
+ if (input.length === 0) return [];
444
+ /* 首元素是 string → 单条件元组 [field, op?, value] */
445
+ if (typeof input[0] === "string") return [input as BodyWhereItem];
446
+ return input as BodyWhereItem[];
447
+ }
448
+
449
+ /** 解析单个 where 条件项 → SQL */
450
+ function bodyCondItem(item: BodyWhereItem, tbl: TblMeta, ctx: Ctx): Cond {
451
+ /* 1) 元组格式 */
452
+ if (Array.isArray(item)) {
453
+ if (item.length === 2) {
454
+ const [field, value] = item as [string, unknown];
455
+ return bodyFieldCond(field, "eq", value, tbl, ctx);
456
+ }
457
+ if (item.length === 3) {
458
+ const [field, op, value] = item as [string, string, unknown];
459
+ return bodyFieldCond(field, op, value, tbl, ctx);
460
+ }
461
+ throw new AppError("QUERY_ERROR", "Invalid where tuple, expected 2 or 3 elements");
462
+ }
463
+
464
+ /* 2) 逻辑组合: { op: "and"/"or", cond: [...] } */
465
+ if ("cond" in item) {
466
+ const logic = (item.op || "and").toUpperCase() as "AND" | "OR";
467
+ const children = (item.cond || []).map((c) => {
468
+ const items = normalizeWhereInput(c);
469
+ /* 如果子输入规范化后是多条件,包装成 AND 组 */
470
+ if (items.length === 1) return bodyCondItem(items[0]!, tbl, ctx);
471
+ const subs = items.map((i) => bodyCondItem(i, tbl, ctx));
472
+ return {
473
+ sql: `(${subs.map((s) => s.sql).join(" AND ")})`,
474
+ values: subs.flatMap((s) => s.values),
475
+ };
476
+ });
477
+ return {
478
+ sql: `(${children.map((c) => c.sql).join(` ${logic} `)})`,
479
+ values: children.flatMap((c) => c.values),
480
+ };
481
+ }
482
+
483
+ /* 3) 对象格式: { field, op, value } */
484
+ return bodyFieldCond(item.field, item.op, item.value, tbl, ctx);
485
+ }
486
+
487
+ /** Body 模式:单字段条件 → SQL */
488
+ function bodyFieldCond(
489
+ field: string, op: string, value: unknown, tbl: TblMeta, ctx: Ctx,
490
+ ): Cond {
491
+ const col = tbl.colMap.get(field);
492
+ if (!col) throw new AppError("QUERY_ERROR", `Unknown column: ${field}`);
493
+
494
+ const f = q(field);
495
+ const o = op.toLowerCase();
496
+
497
+ /* IS NULL / IS NOT NULL */
498
+ if (o === "is") return {sql: `${f} IS NULL`, values: []};
499
+ if (o === "nis" || o === "ns") return {sql: `${f} IS NOT NULL`, values: []};
500
+
501
+ /* LIKE / NOT LIKE */
502
+ if (o === "like" || o === "nlike") {
503
+ const sqlOp = o === "like" ? "LIKE" : "NOT LIKE";
504
+ return {sql: `${f} ${sqlOp} $${ctx.n++}`, values: [String(value)]};
505
+ }
506
+
507
+ /* IN / NOT IN */
508
+ if (o === "in" || o === "nin") {
509
+ const arr = Array.isArray(value) ? value : [value];
510
+ const not = o === "nin" ? "NOT " : "";
511
+ const ph = arr.map(() => `$${ctx.n++}`).join(", ");
512
+ return {
513
+ sql: `${f} ${not}IN (${ph})`,
514
+ values: arr.map((v) => col.isNumeric ? Number(v) : v),
515
+ };
516
+ }
517
+
518
+ /* BETWEEN */
519
+ if (o === "between" || o === "bt") {
520
+ const arr = Array.isArray(value) ? value : [];
521
+ if (arr.length !== 2)
522
+ throw new AppError("QUERY_ERROR", "between requires [lo, hi] array");
523
+ return {
524
+ sql: `${f} BETWEEN $${ctx.n++} AND $${ctx.n++}`,
525
+ values: arr.map((v) => col.isNumeric ? Number(v) : v),
526
+ };
527
+ }
528
+
529
+ /* 标准比较 eq ne ge gt le lt */
530
+ const sqlOp = OP[o];
531
+ if (!sqlOp) throw new AppError("QUERY_ERROR", `Unknown operator: ${op}`);
532
+ const v = col.isNumeric ? Number(value) : value;
533
+ return {sql: `${f} ${sqlOp} $${ctx.n++}`, values: [v]};
534
+ }
535
+
536
+ /* ══════════════════════════════════════════════════════════════
537
+ Body SELECT 解析
538
+ ══════════════════════════════════════════════════════════════ */
539
+
540
+ function bodyBuildSel(items?: BodySelectItem[]): string {
541
+ if (!items || items.length === 0) return "*";
542
+ return items.map((item) => {
543
+ if (typeof item === "string") {
544
+ /* 复用 URL 解析逻辑(冒号分割) */
545
+ const p = item.split(":");
546
+ if (p.length === 1) return q(p[0]!);
547
+ if (p.length === 2) {
548
+ if (AGG_FUNCS.has(p[0]!.toLowerCase())) {
549
+ const fn = p[0]!.toUpperCase();
550
+ return `${fn}(${q(p[1]!)}) AS ${q(`${p[0]!.toLowerCase()}:${p[1]!}`)}`;
551
+ }
552
+ return `${q(p[0]!)} AS ${q(p[1]!)}`;
553
+ }
554
+ return `${p[0]!.toUpperCase()}(${q(p[1]!)}) AS ${q(p[2]!)}`;
555
+ }
556
+ /* 对象格式 { field, alias?, func? } */
557
+ const col = item.func
558
+ ? `${item.func.toUpperCase()}(${q(item.field)})`
559
+ : q(item.field);
560
+ /* func 无 alias 时默认 AS "func:field" */
561
+ if (item.alias) return `${col} AS ${q(item.alias)}`;
562
+ if (item.func) return `${col} AS ${q(`${item.func.toLowerCase()}:${item.field}`)}`;
563
+ return col;
564
+ }).join(", ");
565
+ }
566
+
567
+ /* ══════════════════════════════════════════════════════════════
568
+ Body ORDER BY 解析
569
+ ══════════════════════════════════════════════════════════════ */
570
+
571
+ function bodyBuildOrd(items: BodyOrderItem[]): string {
572
+ return items.map((item) => {
573
+ if (typeof item === "string") {
574
+ if (item.startsWith("asc.")) return `${q(item.slice(4))} ASC`;
575
+ if (item.startsWith("desc.")) return `${q(item.slice(5))} DESC`;
576
+ return `${q(item)} ASC`;
577
+ }
578
+ return `${q(item.field)} ${(item.dir || "asc").toUpperCase()}`;
579
+ }).join(", ");
580
+ }