neopg 0.0.0 → 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/lib/ModelChain.js CHANGED
@@ -1,7 +1,20 @@
1
- 'use strict';
1
+ 'use strict'
2
2
 
3
- const makeId = require('./makeId.js');
4
- const makeTimestamp = require('./makeTimestamp.js');
3
+ const makeId = require('./makeId.js')
4
+ const makeTimestamp = require('./makeTimestamp.js')
5
+ const TransactionScope = require('./TransactionScope.js')
6
+
7
+ // [优化 1] 提取常量定义到类外部,提升性能
8
+ const INT_TYPES = new Set([
9
+ 'int', 'integer', 'smallint', 'bigint',
10
+ 'serial', 'bigserial', 'smallserial',
11
+ 'int2', 'int4', 'int8'
12
+ ])
13
+
14
+ const FLOAT_TYPES = new Set([
15
+ 'float', 'double', 'numeric', 'decimal', 'real',
16
+ 'money', 'double precision', 'float4', 'float8'
17
+ ])
5
18
 
6
19
  /**
7
20
  * ModelChain - 链式查询构建器
@@ -29,6 +42,9 @@ class ModelChain {
29
42
  this._columns = null
30
43
  this._group = null
31
44
  this._lock = null
45
+ this._returning = null
46
+ this._joins = []
47
+ this._group = []
32
48
 
33
49
  // 内部状态标记
34
50
  this._isRaw = !!def.isRaw
@@ -50,68 +66,54 @@ class ModelChain {
50
66
  * 3. .where('age', '>', 18) -> age > 18 (兼容)
51
67
  */
52
68
  where(arg1, arg2, arg3) {
53
- if (!arg1) return this
54
-
55
- // 1. 识别 Postgres Fragment (Query)
56
- // 依据指令:直接检测构造函数名称是否为 'Query'
57
- if (arg1.constructor && arg1.constructor.name === 'Query') {
69
+ if (!arg1) return this
70
+ // 1. Fragment 检测
71
+ if (arg1.constructor && arg1.constructor.name === 'Query') {
58
72
  this._conditions.push(arg1)
59
73
  return this
60
- }
61
-
62
- // 2. 对象写法 .where({ id: 1, name: 'Neo' })
63
- if (typeof arg1 === 'object' && !Array.isArray(arg1)) {
64
- const keys = Object.keys(arg1)
65
- if (keys.length === 0) return this
66
-
67
- for (const key of keys) {
68
- const val = arg1[key];
69
- if (val === undefined) continue
70
-
71
- if (val === null) {
72
- this._conditions.push(this.sql`${this.sql(key)} IS NULL`)
73
- } else if (Array.isArray(val)) {
74
- this._conditions.push(this.sql`${this.sql(key)} IN ${this.sql(val)}`)
75
- } else {
76
- this._conditions.push(this.sql`${this.sql(key)} = ${val}`)
77
- }
78
74
  }
75
+
76
+ // 2. Object 写法
77
+ if (typeof arg1 === 'object' && !Array.isArray(arg1)) {
78
+ for (const k of Object.keys(arg1)) {
79
+ const v = arg1[k]
79
80
 
80
- return this
81
- }
81
+ if (v === undefined) continue
82
82
 
83
- // 3. 字符串/参数写法
84
- if (typeof arg1 === 'string') {
85
- // Case A: .where('id', 123) => id = 123 (默认等于)
86
- if (arg2 !== undefined && arg3 === undefined) {
87
- this._conditions.push(this.sql`${this.sql(arg1)} = ${arg2}`)
88
- return this
89
- }
90
-
91
- // Case B: .where('age', '>', 18)
92
- if (arg3 !== undefined) {
93
- // 注意:中间的操作符必须用 sql.unsafe,因为它不是变量
94
- this._conditions.push(this.sql`${this.sql(arg1)} ${this.sql.unsafe(arg2)} ${arg3}`)
83
+ if (v === null) this._conditions.push(this.sql`${this.sql(k)} IS NULL`)
84
+ else if (Array.isArray(v)) this._conditions.push(this.sql`${this.sql(k)} IN ${this.sql(v)}`)
85
+ else this._conditions.push(this.sql`${this.sql(k)} = ${v}`)
86
+ }
87
+
95
88
  return this
96
89
  }
97
90
 
98
- // Case C: .where('id = ?', 123) (简单兼容)
99
- if (arg1.includes('?') && arg2 !== undefined) {
100
- const parts = arg1.split('?')
101
-
102
- // 只支持单个参数简单替换,复杂的请用 sql``
103
- if (parts.length === 2) {
104
- this._conditions.push(this.sql`${this.sql.unsafe(parts[0])}${arg2}${this.sql.unsafe(parts[1])}`)
105
- return this
106
- }
91
+ // 3. String 写法
92
+ if (typeof arg1 === 'string') {
93
+ // .where('age', '>', 18)
94
+ if (arg3 !== undefined) {
95
+ this._conditions.push(this.sql`${this.sql(arg1)} ${this.sql.unsafe(arg2)} ${arg3}`)
96
+ return this
97
+ }
98
+ // .where('age', 18) -> age = 18
99
+ if (arg2 !== undefined) {
100
+ this._conditions.push(this.sql`${this.sql(arg1)} = ${arg2}`)
101
+ return this
102
+ }
103
+ // .where('id = ?', 123)
104
+ if (arg1.includes('?') && arg2 !== undefined) {
105
+ const p = arg1.split('?');
106
+ if(p.length===2) {
107
+ this._conditions.push(this.sql`${this.sql.unsafe(p[0])}${arg2}${this.sql.unsafe(p[1])}`)
108
+ return this
109
+ }
110
+ }
111
+ // .where('1=1') -> Raw SQL
112
+ // 注意:这里必须用 unsafe,否则 '1=1' 会被当成字符串值处理
113
+ this._conditions.push(this.sql.unsafe(arg1))
107
114
  }
108
-
109
- // Case D: 纯字符串 (视为 Raw SQL)
110
- // .where("status = 'active'")
111
- this._conditions.push(this.sql.unsafe(arg1))
112
- }
113
115
 
114
- return this
116
+ return this
115
117
  }
116
118
 
117
119
  whereIf(condition, arg1, arg2, arg3) {
@@ -131,43 +133,29 @@ class ModelChain {
131
133
  return this
132
134
  }
133
135
 
134
- /**
135
- * 排序
136
- * .order(sql`create_time DESC`)
137
- * .order('create_time', 'DESC')
138
- * .order({ create_time: 'DESC' })
139
- */
140
- order(arg1, arg2) {
141
- if (!arg1) return this
142
-
143
- // 1. Fragment
144
- if (arg1.constructor && arg1.constructor.name === 'Query') {
145
- this._order.push(arg1)
146
- return this
147
- }
136
+ orderby(a, b) {
137
+ if(!a) return this
148
138
 
149
- // 2. Object { id: 'DESC' }
150
- if (typeof arg1 === 'object') {
151
- for (const key in arg1) {
152
- const dir = arg1[key].toUpperCase()
153
- this._order.push(this.sql`${this.sql(key)} ${this.sql.unsafe(dir)}`)
139
+ if(a.constructor && a.constructor.name==='Query') {
140
+ this._order.push(a)
141
+ return this
154
142
  }
155
143
 
156
- return this
157
- }
144
+ if(typeof a==='object') {
145
+ for(const k in a) {
146
+ this._order.push(this.sql`${this.sql(k)} ${this.sql.unsafe(a[k].toUpperCase())}`)
147
+ }
148
+
149
+ return this
150
+ }
158
151
 
159
- // 3. String ('id', 'DESC')
160
- if (typeof arg1 === 'string') {
161
- const dir = arg2 ? arg2.toUpperCase() : 'ASC'
162
- // 检查 arg1 是否包含空格 (e.g. "id desc")
163
- if (arg1.includes(' ')) {
164
- this._order.push(this.sql.unsafe(arg1))
165
- } else {
166
- this._order.push(this.sql`${this.sql(arg1)} ${this.sql.unsafe(dir)}`)
152
+ if(typeof a==='string') {
153
+ const d = b ? b.toUpperCase() : 'ASC'
154
+ if(a.includes(' ')) this._order.push(this.sql.unsafe(a));
155
+ else this._order.push(this.sql`${this.sql(a)} ${this.sql.unsafe(d)}`);
167
156
  }
168
- }
169
157
 
170
- return this
158
+ return this
171
159
  }
172
160
 
173
161
  limit(count, offset = 0) {
@@ -190,49 +178,91 @@ class ModelChain {
190
178
  return this
191
179
  }
192
180
 
193
- // --- 核心:执行方法 ---
181
+ returning(cols) {
182
+ if (!cols) return this
194
183
 
195
- async find() {
196
- const tableFragment = this.sql(this.tableName)
184
+ if (typeof cols === 'string') {
185
+ // 支持 'id, name' 写法
186
+ this._returning = cols.split(',').map(s => s.trim()).filter(s => s)
187
+ } else if (Array.isArray(cols)) {
188
+ this._returning = cols
189
+ }
190
+
191
+ return this
192
+ }
197
193
 
198
- const colsFragment = this._columns
199
- ? this.sql(this._columns)
200
- : this.sql`*`
194
+ // --- 构建 RETURNING 片段 ---
195
+ _buildReturning() {
196
+ // 如果没有设置 returning,默认不返回数据 (节省性能)
197
+ // 注意:这意味着默认 insert/update 返回的是 Result 对象(包含 count),而不是行数据
198
+ if (!this._returning || this._returning.length === 0) {
199
+ return this.sql``
200
+ }
201
201
 
202
- const whereFragment = this._conditions.length
203
- ? this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
204
- : this.sql``
202
+ // 特殊处理 '*': 用户显式要求 returning('*')
203
+ // 如果直接用 this.sql(['*']) 会被转义为 "*",导致错误
204
+ if (this._returning.length === 1 && this._returning[0] === '*') {
205
+ return this.sql`RETURNING *`
206
+ }
205
207
 
206
- const orderFragment = this._order.length
207
- ? this.sql`ORDER BY ${this.sql(this._order)}`
208
- : this.sql``
209
-
210
- const limitFragment = this._limit
211
- ? this.sql`LIMIT ${this._limit}`
212
- : this.sql``
213
-
214
- const offsetFragment = this._offset
215
- ? this.sql`OFFSET ${this._offset}`
216
- : this.sql``
208
+ // 普通字段:利用 postgres.js 自动转义标识符
209
+ return this.sql`RETURNING ${this.sql(this._returning)}`
210
+ }
211
+
212
+ // --- 辅助:构建 Where 片段 (修复 Bug 的核心) ---
213
+ _buildWhere() {
214
+ const len = this._conditions.length
215
+ if (len === 0) return this.sql``
217
216
 
218
- const lockFragment = this._lock || this.sql``
219
-
220
- const fullTable = this.sql`${this.sql(this.schema)}.${tableFragment}`
221
-
222
- return await this.sql`
223
- SELECT ${colsFragment}
224
- FROM ${fullTable}
225
- ${whereFragment}
226
- ${orderFragment}
227
- ${limitFragment}
228
- ${offsetFragment}
229
- ${lockFragment}
230
- `
217
+ // 只有一个条件,直接返回,零开销
218
+ if (len === 1) {
219
+ return this.sql`WHERE ${this._conditions[0]}`
220
+ }
221
+
222
+ // 预分配数组:N个条件需要 N-1 个 'AND',总长 2N-1
223
+ // 使用 new Array 预分配内存,比 push 更快
224
+ const parts = new Array(len * 2 - 1)
225
+ const AND = this.sql.unsafe(' AND ')
226
+
227
+ for (let i = 0; i < len; i++) {
228
+ // 偶数位放条件
229
+ parts[i * 2] = this._conditions[i]
230
+ // 奇数位放 AND (除了最后一位)
231
+ if (i < len - 1) {
232
+ parts[i * 2 + 1] = AND
233
+ }
234
+ }
235
+
236
+ // postgres.js 会自动展开这个扁平数组,性能极高
237
+ return this.sql`WHERE ${parts}`
238
+ }
239
+
240
+ // --- 辅助:构建 Order 片段 (修复 Bug) ---
241
+ _buildOrder() {
242
+ if (this._order.length === 0) return this.sql``
243
+ // 数组直接传入模板,postgres.js 默认用逗号连接,这正是 ORDER BY 需要的
244
+ // 不能用 this.sql(this._order),那样会试图转义为标识符
245
+ return this.sql`ORDER BY ${this._order}`
231
246
  }
232
247
 
233
- // Thenable 接口
234
- then(onFulfilled, onRejected) {
235
- return this.find().then(onFulfilled, onRejected)
248
+ // --- 核心:执行方法 ---
249
+
250
+ async find() {
251
+ const t = this.sql(this.tableName)
252
+ const c = this._columns ? this.sql(this._columns) : this.sql`*`
253
+
254
+ // 修复:使用新方法构建
255
+ const w = this._buildWhere()
256
+ const o = this._buildOrder()
257
+ const j = this._buildJoins()
258
+ const g = this._buildGroup()
259
+
260
+ const l = this._limit ? this.sql`LIMIT ${this._limit}` : this.sql``
261
+ const off = this._offset ? this.sql`OFFSET ${this._offset}` : this.sql``
262
+ const lck = this._lock || this.sql``
263
+ const ft = this.sql`${this.sql(this.schema)}.${t}`
264
+
265
+ return await this.sql`SELECT ${c} FROM ${ft} ${j} ${w} ${g} ${o} ${l} ${off} ${lck}`
236
266
  }
237
267
 
238
268
  async get() {
@@ -242,22 +272,19 @@ class ModelChain {
242
272
  }
243
273
 
244
274
  async count() {
245
- const tableFragment = this.sql(this.tableName);
246
- const whereFragment = this._conditions.length
247
- ? this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
248
- : this.sql``
275
+ const t = this.sql(this.tableName)
276
+
277
+ const w = this._buildWhere()
278
+ const ft = this.sql`${this.sql(this.schema)}.${t}`
279
+ const j = this._buildJoins()
249
280
 
250
- const fullTable = this.sql`${this.sql(this.schema)}.${tableFragment}`
281
+ const r = await this.sql`SELECT count(*) as total FROM ${ft} ${j} ${w}`
251
282
 
252
- const result = await this.sql`
253
- SELECT count(*) as total FROM ${fullTable} ${whereFragment}
254
- `
283
+ if (r.length === 0) return 0
255
284
 
256
- return parseInt(result[0].total)
285
+ return parseInt(r[0].total)
257
286
  }
258
287
 
259
- // --- 写入方法 ---
260
-
261
288
  async insert(data) {
262
289
  const isArray = Array.isArray(data)
263
290
  const inputs = isArray ? data : [data]
@@ -269,53 +296,299 @@ class ModelChain {
269
296
 
270
297
  const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
271
298
 
272
- const result = await this.sql`
273
- INSERT INTO ${fullTable} ${this.sql(inputs)}
274
- RETURNING *
275
- `
299
+ // [修改] 动态构建 returning
300
+ const retFragment = this._buildReturning()
301
+
302
+ const result = await this.sql`INSERT INTO ${fullTable} ${this.sql(inputs)} ${retFragment}`
276
303
 
277
- if (!isArray && result.length > 0) return result[0]
304
+ // 如果有 returning 数据,result 是数组(包含行);否则 result Result 对象(包含 count)
305
+ // 逻辑保持兼容:如果用户请求了数据,且是单条插入,返回对象;否则返回数组
306
+ if (this._returning && this._returning.length > 0) {
307
+ if (!isArray && result.length === 1) {
308
+ return result[0]
309
+ }
278
310
 
311
+ return result
312
+ }
313
+
314
+ // 如果没有 returning,返回 postgres 原生结果 (包含 count 等信息)
315
+ // 测试发现如果没有returning则返回的是空数组
279
316
  return result
280
317
  }
281
318
 
282
319
  async update(data) {
283
320
  if (!data || Object.keys(data).length === 0) throw new Error('[NeoPG] Update data cannot be empty')
321
+ if (this.def) { this._prepareDataForUpdate(data) }
322
+
323
+ if (this._conditions.length === 0) throw new Error('[NeoPG] UPDATE requires a WHERE condition')
324
+
325
+ const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
326
+ // 修复:使用新方法构建
327
+ const whereFragment = this._buildWhere()
328
+
329
+ // [修改] 动态构建 returning
330
+ const retFragment = this._buildReturning()
331
+
332
+ const result = await this.sql`UPDATE ${fullTable} SET ${this.sql(data)} ${whereFragment} ${retFragment}`
284
333
 
285
- if (this.def) {
286
- this._prepareDataForUpdate(data)
287
- }
334
+ if (this._returning && this._returning.length > 0) {
335
+ if (result.length === 1) return result[0]
288
336
 
289
- if (this._conditions.length === 0) {
290
- throw new Error('[NeoPG] UPDATE requires a WHERE condition')
337
+ return result
291
338
  }
292
339
 
293
- const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
294
- const whereFragment = this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
295
-
296
- // 使用 sql(data) 自动处理 set a=1, b=2
297
- return await this.sql`
298
- UPDATE ${fullTable}
299
- SET ${this.sql(data)}
300
- ${whereFragment}
301
- RETURNING *
302
- `
340
+ return result
303
341
  }
304
342
 
305
343
  async delete() {
306
- if (this._conditions.length === 0) {
307
- throw new Error('[NeoPG] DELETE requires a WHERE condition')
344
+ if (this._conditions.length === 0) throw new Error('[NeoPG] DELETE requires a WHERE condition')
345
+ const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
346
+ // 修复:使用新方法构建
347
+ const whereFragment = this._buildWhere()
348
+
349
+ const retFragment = this._buildReturning()
350
+
351
+ return await this.sql`DELETE FROM ${fullTable} ${whereFragment} ${retFragment}`
352
+ }
353
+
354
+ async transaction(callback) {
355
+ return this.ctx.transaction(callback)
356
+ /* return await this.sql.begin(async (trxSql) => {
357
+ const scope = new TransactionScope(this.ctx, trxSql)
358
+ return await callback(scope)
359
+ }) */
360
+ }
361
+
362
+ begin(callback) {
363
+ return this.ctx.transaction(callback)
364
+ }
365
+
366
+ /**
367
+ * 内部通用 Join 添加器
368
+ * @param {string} type - 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN'
369
+ * @param {string|Object} table - 表名 或 sql`fragment`
370
+ * @param {string|Object} on - 条件字符串 或 sql`fragment`
371
+ */
372
+ _addJoin(type, table, on) {
373
+ let tableFragment
374
+ let onFragment
375
+
376
+ // 1. 处理 Table
377
+ if (table.constructor && table.constructor.name === 'Query') {
378
+ tableFragment = table
379
+ } else {
380
+ // 默认作为当前 Schema 下的表名处理 "public"."table"
381
+ // 如果需要跨 Schema (e.g. "other.table"),请用户传入 sql`other.table`
382
+ tableFragment = this.sql(table)
308
383
  }
309
384
 
310
- const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
385
+ // 2. 处理 ON 条件
386
+ if (on.constructor && on.constructor.name === 'Query') {
387
+ onFragment = on;
388
+ } else {
389
+ // 字符串情况,视为 Raw SQL (e.g. "u.id = p.uid")
390
+ // 因为 ON 条件通常包含操作符,无法简单参数化,必须 unsafe
391
+ onFragment = this.sql.unsafe(on);
392
+ }
393
+
394
+ // 3. 构建单个 Join 片段
395
+ // 格式: TYPE + table + ON + condition
396
+ const joinFragment = this.sql`${this.sql.unsafe(type)} ${tableFragment} ON ${onFragment}`
397
+
398
+ this._joins.push(joinFragment)
399
+ return this
400
+ }
401
+
402
+ join(table, on) {
403
+ return this._addJoin('INNER JOIN', table, on)
404
+ }
405
+
406
+ innerJoin(table, on) {
407
+ return this._addJoin('INNER JOIN', table, on)
408
+ }
409
+
410
+ leftJoin(table, on) {
411
+ return this._addJoin('LEFT JOIN', table, on)
412
+ }
413
+
414
+ rightJoin(table, on) {
415
+ return this._addJoin('RIGHT JOIN', table, on)
416
+ }
417
+
418
+ fullJoin(table, on) {
419
+ return this._addJoin('FULL OUTER JOIN', table, on)
420
+ }
421
+
422
+ _buildJoins() {
423
+ const len = this._joins.length
424
+ if (len === 0) return this.sql``
425
+
426
+ // 只有一个 Join,直接返回
427
+ if (len === 1) {
428
+ return this._joins[0]
429
+ }
430
+
431
+ // 多个 Join,必须用空格连接,不能用逗号
432
+ // 采用“平铺数组”高性能方案
433
+ const parts = new Array(len * 2 - 1)
434
+ const SPACE = this.sql.unsafe(' ')
435
+
436
+ for (let i = 0; i < len; i++) {
437
+ parts[i * 2] = this._joins[i]
438
+ if (i < len - 1) {
439
+ parts[i * 2 + 1] = SPACE
440
+ }
441
+ }
442
+
443
+ return this.sql`${parts}`
444
+ }
445
+
446
+ /**
447
+ * 添加 Group By 条件
448
+ * .group('category_id')
449
+ * .group('category_id, type')
450
+ * .group(['id', 'name'])
451
+ */
452
+ group(arg) {
453
+ if (!arg) return this
454
+
455
+ // 1. Fragment
456
+ if (arg.constructor && arg.constructor.name === 'Query') {
457
+ this._group.push(arg)
458
+ return this
459
+ }
460
+
461
+ // 2. Array
462
+ if (Array.isArray(arg)) {
463
+ arg.forEach(f => this.group(f))
464
+ return this
465
+ }
311
466
 
312
- const whereFragment = this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
467
+ // 3. String
468
+ if (typeof arg === 'string') {
469
+ if (arg.includes(',')) {
470
+ // 'id, name' -> 拆分
471
+ arg.split(',').map(s => s.trim()).filter(s=>s).forEach(s => {
472
+ this._group.push(this.sql(s))
473
+ })
474
+ } else {
475
+ // 单个字段
476
+ this._group.push(this.sql(arg))
477
+ }
478
+ }
479
+
480
+ return this
481
+ }
313
482
 
314
- return await this.sql`
315
- DELETE FROM ${fullTable}
316
- ${whereFragment}
317
- RETURNING *
483
+ // 构建 Group 片段
484
+ _buildGroup() {
485
+ if (this._group.length === 0) return this.sql``
486
+
487
+ // postgres.js 模板数组默认用逗号连接,正好符合 GROUP BY 语法
488
+ return this.sql`GROUP BY ${this._group}`
489
+ }
490
+
491
+ // --- 聚合函数 ---
492
+
493
+ async min(field) {
494
+ return this._aggregate('MIN', field)
495
+ }
496
+
497
+ async max(field) {
498
+ return this._aggregate('MAX', field)
499
+ }
500
+
501
+ async sum(field) {
502
+ return this._aggregate('SUM', field)
503
+ }
504
+
505
+ async avg(field) {
506
+ return this._aggregate('AVG', field)
507
+ }
508
+
509
+ /**
510
+ * 通用聚合执行器
511
+ * @param {string} func - MIN, MAX, SUM, AVG
512
+ * @param {string} field - 列名
513
+ */
514
+ async _aggregate(func, field) {
515
+ if (!field) throw new Error(`[NeoPG] ${func} requires a field name.`)
516
+
517
+ const t = this.sql(this.tableName)
518
+ const w = this._buildWhere()
519
+ const j = this._buildJoins()
520
+ const ft = this.sql`${this.sql(this.schema)}.${t}`
521
+
522
+ // 处理字段名 (可能是 'age' 也可能是 'users.age')
523
+ let colFragment;
524
+ if (field.constructor && field.constructor.name === 'Query') {
525
+ colFragment = field
526
+ } else {
527
+ colFragment = this.sql(field)
528
+ }
529
+
530
+ // SELECT MIN(age) as val ...
531
+ const query = this.sql`
532
+ SELECT ${this.sql.unsafe(func)}(${colFragment}) as val
533
+ FROM ${ft} ${j} ${w}
318
534
  `
535
+
536
+ const result = await query
537
+ const val = result.length > 0 ? result[0].val : null
538
+
539
+ if (val === null) return null;
540
+
541
+ // 智能类型转换
542
+ return this._convertAggregateValue(val, field, func)
543
+ }
544
+
545
+ /**
546
+ * 智能转换聚合结果类型
547
+ * Postgres 对于 SUM/AVG/COUNT 经常返回字符串 (BigInt/Numeric),我们需要转回 Number
548
+ */
549
+ _convertAggregateValue(val, field, func) {
550
+ // 1. AVG 始终是浮点数
551
+ if (func === 'AVG') {
552
+ return parseFloat(val)
553
+ }
554
+
555
+ // 如果是 Raw Fragment,无法推断类型,直接返回原值(通常是 String)
556
+ if (typeof field !== 'string') return val
557
+
558
+ // 2. 尝试从 ModelDef 获取列定义
559
+ // field 可能是 'age' 也可能是 'u.age' (别名暂不支持自动推断,这里只处理简单列名)
560
+ const colDef = this.def && this.def.columns ? this.def.columns[field] : null
561
+
562
+ // 如果不知道列定义,尝试尽力猜测
563
+ if (!colDef) {
564
+ // 如果 val 是字符串且长得像数字
565
+ if (typeof val === 'string' && !isNaN(val)) {
566
+ // SUM 默认为数字
567
+ if (func === 'SUM') return parseFloat(val)
568
+ }
569
+
570
+ return val
571
+ }
572
+
573
+ // 5. [优化] 精确类型匹配
574
+ // 处理 'numeric(10,2)' -> 'numeric'
575
+ // 处理 'integer' -> 'integer'
576
+ const rawType = colDef.type.toLowerCase()
577
+ const parenIndex = rawType.indexOf('(')
578
+ const baseType = parenIndex > 0 ? rawType.substring(0, parenIndex).trim() : rawType
579
+
580
+ // 整数匹配
581
+ if (INT_TYPES.has(baseType)) {
582
+ return parseInt(val, 10)
583
+ }
584
+
585
+ // 浮点数匹配
586
+ if (FLOAT_TYPES.has(baseType)) {
587
+ return parseFloat(val)
588
+ }
589
+
590
+ // 其他 (Date, String, Boolean) 原样返回
591
+ return val
319
592
  }
320
593
 
321
594
  // --- 内部辅助方法 ---