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/LICENSE +21 -0
- package/README.cn.md +336 -0
- package/README.md +336 -0
- package/images/neopg-programming.jpeg +0 -0
- package/images/neopg.png +0 -0
- package/lib/ModelChain.js +434 -161
- package/lib/ModelDef.js +4 -1
- package/lib/NeoPG.js +57 -3
- package/lib/SchemaSync.js +77 -36
- package/lib/TransactionScope.js +6 -2
- package/package.json +9 -2
- package/test/test-db.js +194 -21
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
81
|
-
}
|
|
81
|
+
if (v === undefined) continue
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
//
|
|
99
|
-
if (arg1
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
// 特殊处理 '*': 用户显式要求 returning('*')
|
|
203
|
+
// 如果直接用 this.sql(['*']) 会被转义为 "*",导致错误
|
|
204
|
+
if (this._returning.length === 1 && this._returning[0] === '*') {
|
|
205
|
+
return this.sql`RETURNING *`
|
|
206
|
+
}
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
281
|
+
const r = await this.sql`SELECT count(*) as total FROM ${ft} ${j} ${w}`
|
|
251
282
|
|
|
252
|
-
|
|
253
|
-
SELECT count(*) as total FROM ${fullTable} ${whereFragment}
|
|
254
|
-
`
|
|
283
|
+
if (r.length === 0) return 0
|
|
255
284
|
|
|
256
|
-
return parseInt(
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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.
|
|
286
|
-
|
|
287
|
-
}
|
|
334
|
+
if (this._returning && this._returning.length > 0) {
|
|
335
|
+
if (result.length === 1) return result[0]
|
|
288
336
|
|
|
289
|
-
|
|
290
|
-
throw new Error('[NeoPG] UPDATE requires a WHERE condition')
|
|
337
|
+
return result
|
|
291
338
|
}
|
|
292
339
|
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
// --- 内部辅助方法 ---
|