neopg 0.0.1 → 1.0.1
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 +415 -0
- package/README.md +415 -0
- package/bin/neopg-model.js +207 -0
- package/images/neopg-programming.jpeg +0 -0
- package/images/neopg.png +0 -0
- package/lib/ModelChain.js +323 -15
- package/lib/NeoPG.js +60 -2
- package/lib/SchemaSync.js +62 -25
- package/lib/TransactionScope.js +4 -0
- package/package.json +12 -2
- package/test/test-db.js +57 -17
package/lib/ModelChain.js
CHANGED
|
@@ -4,6 +4,18 @@ const makeId = require('./makeId.js')
|
|
|
4
4
|
const makeTimestamp = require('./makeTimestamp.js')
|
|
5
5
|
const TransactionScope = require('./TransactionScope.js')
|
|
6
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
|
+
])
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* ModelChain - 链式查询构建器
|
|
9
21
|
* 负责运行时的查询构建、SQL 拼装和结果处理。
|
|
@@ -30,6 +42,9 @@ class ModelChain {
|
|
|
30
42
|
this._columns = null
|
|
31
43
|
this._group = null
|
|
32
44
|
this._lock = null
|
|
45
|
+
this._returning = null
|
|
46
|
+
this._joins = []
|
|
47
|
+
this._group = []
|
|
33
48
|
|
|
34
49
|
// 内部状态标记
|
|
35
50
|
this._isRaw = !!def.isRaw
|
|
@@ -163,6 +178,37 @@ class ModelChain {
|
|
|
163
178
|
return this
|
|
164
179
|
}
|
|
165
180
|
|
|
181
|
+
returning(cols) {
|
|
182
|
+
if (!cols) return this
|
|
183
|
+
|
|
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
|
+
}
|
|
193
|
+
|
|
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
|
+
|
|
202
|
+
// 特殊处理 '*': 用户显式要求 returning('*')
|
|
203
|
+
// 如果直接用 this.sql(['*']) 会被转义为 "*",导致错误
|
|
204
|
+
if (this._returning.length === 1 && this._returning[0] === '*') {
|
|
205
|
+
return this.sql`RETURNING *`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 普通字段:利用 postgres.js 自动转义标识符
|
|
209
|
+
return this.sql`RETURNING ${this.sql(this._returning)}`
|
|
210
|
+
}
|
|
211
|
+
|
|
166
212
|
// --- 辅助:构建 Where 片段 (修复 Bug 的核心) ---
|
|
167
213
|
_buildWhere() {
|
|
168
214
|
const len = this._conditions.length
|
|
@@ -208,18 +254,15 @@ class ModelChain {
|
|
|
208
254
|
// 修复:使用新方法构建
|
|
209
255
|
const w = this._buildWhere()
|
|
210
256
|
const o = this._buildOrder()
|
|
257
|
+
const j = this._buildJoins()
|
|
258
|
+
const g = this._buildGroup()
|
|
211
259
|
|
|
212
260
|
const l = this._limit ? this.sql`LIMIT ${this._limit}` : this.sql``
|
|
213
261
|
const off = this._offset ? this.sql`OFFSET ${this._offset}` : this.sql``
|
|
214
262
|
const lck = this._lock || this.sql``
|
|
215
263
|
const ft = this.sql`${this.sql(this.schema)}.${t}`
|
|
216
264
|
|
|
217
|
-
return await this.sql`SELECT ${c} FROM ${ft} ${w} ${o} ${l} ${off} ${lck}`
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Thenable 接口
|
|
221
|
-
then(onFulfilled, onRejected) {
|
|
222
|
-
return this.find().then(onFulfilled, onRejected)
|
|
265
|
+
return await this.sql`SELECT ${c} FROM ${ft} ${j} ${w} ${g} ${o} ${l} ${off} ${lck}`
|
|
223
266
|
}
|
|
224
267
|
|
|
225
268
|
async get() {
|
|
@@ -229,12 +272,16 @@ class ModelChain {
|
|
|
229
272
|
}
|
|
230
273
|
|
|
231
274
|
async count() {
|
|
232
|
-
const t = this.sql(this.tableName)
|
|
233
|
-
|
|
275
|
+
const t = this.sql(this.tableName)
|
|
276
|
+
|
|
234
277
|
const w = this._buildWhere()
|
|
235
278
|
const ft = this.sql`${this.sql(this.schema)}.${t}`
|
|
279
|
+
const j = this._buildJoins()
|
|
236
280
|
|
|
237
|
-
const r = await this.sql`SELECT count(*) as total FROM ${ft} ${w}`
|
|
281
|
+
const r = await this.sql`SELECT count(*) as total FROM ${ft} ${j} ${w}`
|
|
282
|
+
|
|
283
|
+
if (r.length === 0) return 0
|
|
284
|
+
|
|
238
285
|
return parseInt(r[0].total)
|
|
239
286
|
}
|
|
240
287
|
|
|
@@ -248,9 +295,24 @@ class ModelChain {
|
|
|
248
295
|
}
|
|
249
296
|
|
|
250
297
|
const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
|
|
251
|
-
const result = await this.sql`INSERT INTO ${fullTable} ${this.sql(inputs)} RETURNING *`
|
|
252
298
|
|
|
253
|
-
|
|
299
|
+
// [修改] 动态构建 returning
|
|
300
|
+
const retFragment = this._buildReturning()
|
|
301
|
+
|
|
302
|
+
const result = await this.sql`INSERT INTO ${fullTable} ${this.sql(inputs)} ${retFragment}`
|
|
303
|
+
|
|
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
|
+
}
|
|
310
|
+
|
|
311
|
+
return result
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 如果没有 returning,返回 postgres 原生结果 (包含 count 等信息)
|
|
315
|
+
// 测试发现如果没有returning则返回的是空数组
|
|
254
316
|
return result
|
|
255
317
|
}
|
|
256
318
|
|
|
@@ -264,7 +326,18 @@ class ModelChain {
|
|
|
264
326
|
// 修复:使用新方法构建
|
|
265
327
|
const whereFragment = this._buildWhere()
|
|
266
328
|
|
|
267
|
-
|
|
329
|
+
// [修改] 动态构建 returning
|
|
330
|
+
const retFragment = this._buildReturning()
|
|
331
|
+
|
|
332
|
+
const result = await this.sql`UPDATE ${fullTable} SET ${this.sql(data)} ${whereFragment} ${retFragment}`
|
|
333
|
+
|
|
334
|
+
if (this._returning && this._returning.length > 0) {
|
|
335
|
+
if (result.length === 1) return result[0]
|
|
336
|
+
|
|
337
|
+
return result
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result
|
|
268
341
|
}
|
|
269
342
|
|
|
270
343
|
async delete() {
|
|
@@ -273,14 +346,249 @@ class ModelChain {
|
|
|
273
346
|
// 修复:使用新方法构建
|
|
274
347
|
const whereFragment = this._buildWhere()
|
|
275
348
|
|
|
276
|
-
|
|
349
|
+
const retFragment = this._buildReturning()
|
|
350
|
+
|
|
351
|
+
return await this.sql`DELETE FROM ${fullTable} ${whereFragment} ${retFragment}`
|
|
277
352
|
}
|
|
278
353
|
|
|
279
354
|
async transaction(callback) {
|
|
280
|
-
return
|
|
355
|
+
return this.ctx.transaction(callback)
|
|
356
|
+
/* return await this.sql.begin(async (trxSql) => {
|
|
281
357
|
const scope = new TransactionScope(this.ctx, trxSql)
|
|
282
358
|
return await callback(scope)
|
|
283
|
-
})
|
|
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)
|
|
383
|
+
}
|
|
384
|
+
|
|
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
|
+
}
|
|
466
|
+
|
|
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
|
+
}
|
|
482
|
+
|
|
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}
|
|
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
|
|
284
592
|
}
|
|
285
593
|
|
|
286
594
|
// --- 内部辅助方法 ---
|
package/lib/NeoPG.js
CHANGED
|
@@ -33,8 +33,9 @@ class NeoPG {
|
|
|
33
33
|
|
|
34
34
|
// --- 注册 ---
|
|
35
35
|
|
|
36
|
-
add(input) {
|
|
37
|
-
let ModelClass
|
|
36
|
+
add(input, is_reset=false) {
|
|
37
|
+
let ModelClass
|
|
38
|
+
|
|
38
39
|
if (typeof input === 'function') {
|
|
39
40
|
ModelClass = input
|
|
40
41
|
} else {
|
|
@@ -42,10 +43,16 @@ class NeoPG {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const rawSchema = ModelClass.schema
|
|
46
|
+
|
|
45
47
|
if (!rawSchema) throw new Error(`[NeoPG] Missing static schema for ${ModelClass.name}`)
|
|
46
48
|
|
|
47
49
|
const def = new ModelDef(rawSchema)
|
|
48
50
|
|
|
51
|
+
//已经存在又不是更新,则报错
|
|
52
|
+
if (!is_reset && this.registry.has(def.modelName)) {
|
|
53
|
+
throw new Error(`[NeoPG] modelName conflict: ${def.modelName}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
this.registry.set(def.modelName, {
|
|
50
57
|
Class: ModelClass,
|
|
51
58
|
def: def
|
|
@@ -54,6 +61,14 @@ class NeoPG {
|
|
|
54
61
|
return this
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
define(model) {
|
|
65
|
+
return this.add(model)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
set(model) {
|
|
69
|
+
return this.add(model, true)
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
// --- 事务 ---
|
|
58
73
|
async transaction(callback) {
|
|
59
74
|
return await this.driver.begin(async (trxSql) => {
|
|
@@ -62,6 +77,10 @@ class NeoPG {
|
|
|
62
77
|
})
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
begin(callback) {
|
|
81
|
+
return this.transaction(callback)
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
// --- 同步 ---
|
|
66
85
|
async sync(options = {}) {
|
|
67
86
|
if (!options || typeof options !== 'object') {
|
|
@@ -75,6 +94,45 @@ class NeoPG {
|
|
|
75
94
|
}
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 监听 Postgres 消息通道
|
|
99
|
+
* @param {string} channel - 通道名称
|
|
100
|
+
* @param {Function} callback - (payload) => {}
|
|
101
|
+
* @returns {Object} 包含 unlisten 方法的对象
|
|
102
|
+
*/
|
|
103
|
+
async listen(channel, callback) {
|
|
104
|
+
// postgres.js 的 listen 返回一个 Promise<void>
|
|
105
|
+
// 但它内部会维持连接。我们需要提供一种方式来取消监听。
|
|
106
|
+
// postgres.js v3 使用 sql.listen(channel, cb) 并返回一个 state 对象用于 close
|
|
107
|
+
const listener = await this.sql.listen(channel, (payload) => {
|
|
108
|
+
// 可以在这里统一处理 JSON 解析等
|
|
109
|
+
try {
|
|
110
|
+
const data = JSON.parse(payload)
|
|
111
|
+
callback(data)
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// 无法解析则返回原始字符串
|
|
114
|
+
callback(payload)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
// 返回一个句柄用于取消监听
|
|
120
|
+
close: () => listener.unlisten()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 发送通知
|
|
126
|
+
* @param {string} channel
|
|
127
|
+
* @param {string|Object} payload
|
|
128
|
+
*/
|
|
129
|
+
async notify(channel, payload) {
|
|
130
|
+
const data = typeof payload === 'object' ? JSON.stringify(payload) : payload
|
|
131
|
+
|
|
132
|
+
// 使用 sql.notify 是最高效的
|
|
133
|
+
await this.sql.notify(channel, data)
|
|
134
|
+
}
|
|
135
|
+
|
|
78
136
|
async close() {
|
|
79
137
|
await this.driver.end()
|
|
80
138
|
}
|
package/lib/SchemaSync.js
CHANGED
|
@@ -47,6 +47,18 @@ class SchemaSync {
|
|
|
47
47
|
const tableName = def.tableName;
|
|
48
48
|
const curTableName = `${schema}.${tableName}`;
|
|
49
49
|
|
|
50
|
+
// [递归锁初始化] 记录本次同步过程中已经处理过的 Model,防止死循环
|
|
51
|
+
if (!options.syncedModels) {
|
|
52
|
+
options.syncedModels = new Set();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 如果该表在本次递归链中已经同步过,则跳过
|
|
56
|
+
if (options.syncedModels.has(def.modelName)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// 标记当前表已开始同步
|
|
60
|
+
options.syncedModels.add(def.modelName);
|
|
61
|
+
|
|
50
62
|
if (debug) console.log(`检测数据表 ${tableName} 的column...`);
|
|
51
63
|
|
|
52
64
|
// 0. 检查列定义的合法性 (移植自 _checkFixColumn)
|
|
@@ -76,7 +88,7 @@ class SchemaSync {
|
|
|
76
88
|
// 同步索引、约束、外键
|
|
77
89
|
await this.syncIndex(sql, def, schema, curTableName, debug);
|
|
78
90
|
await this.syncUnique(sql, def, schema, curTableName, debug);
|
|
79
|
-
await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx);
|
|
91
|
+
await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options);
|
|
80
92
|
return;
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -84,7 +96,7 @@ class SchemaSync {
|
|
|
84
96
|
// 获取现有列信息
|
|
85
97
|
const cols = await sql`
|
|
86
98
|
SELECT column_name, data_type, column_default, character_maximum_length,
|
|
87
|
-
numeric_precision, numeric_scale, is_nullable
|
|
99
|
+
numeric_precision, numeric_scale, is_nullable, is_generated
|
|
88
100
|
FROM information_schema.columns
|
|
89
101
|
WHERE table_name = ${tableName}
|
|
90
102
|
AND table_schema = ${schema}
|
|
@@ -103,7 +115,7 @@ class SchemaSync {
|
|
|
103
115
|
await this.syncIndex(sql, def, schema, curTableName, debug);
|
|
104
116
|
await this.syncUnique(sql, def, schema, curTableName, debug);
|
|
105
117
|
await this.autoRemoveIndex(sql, def, schema, tableName, debug);
|
|
106
|
-
await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx);
|
|
118
|
+
await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options);
|
|
107
119
|
|
|
108
120
|
if (debug) console.log(` - 表结构同步完成 (${tableName}) - `);
|
|
109
121
|
}
|
|
@@ -263,12 +275,31 @@ class SchemaSync {
|
|
|
263
275
|
|
|
264
276
|
// 7. Drop Not Exist (Force Mode)
|
|
265
277
|
if (dropNotExistCol) {
|
|
266
|
-
for (let
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
for (let dbColName in inf) {
|
|
279
|
+
// 如果代码中没定义,且不是刚改名的
|
|
280
|
+
if (!def.columns[dbColName] && !renameTable[dbColName]) {
|
|
281
|
+
|
|
282
|
+
const dbCol = inf[dbColName];
|
|
283
|
+
|
|
284
|
+
// [核心逻辑] 如果是数据库层面的虚拟列 (is_generated == 'ALWAYS'),豁免,不删除
|
|
285
|
+
if (dbCol.is_generated === 'ALWAYS') {
|
|
286
|
+
if (debug) console.log(`[NeoPG] Ignoring DB-only generated column: ${dbColName}`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// [额外建议] 如果是 identity 列 (自增列的一种新形式),通常也建议豁免,防止误删
|
|
291
|
+
// if (dbCol.is_identity === 'YES') continue;
|
|
292
|
+
|
|
293
|
+
if (debug) console.log(`Deleting unused column: ${dbColName}`);
|
|
294
|
+
await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(dbColName)}`);
|
|
269
295
|
}
|
|
270
296
|
}
|
|
271
297
|
}
|
|
298
|
+
/* for (let k in inf) {
|
|
299
|
+
if (!def.columns[k] && !renameTable[k]) {
|
|
300
|
+
await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(k)}`);
|
|
301
|
+
}
|
|
302
|
+
} */
|
|
272
303
|
}
|
|
273
304
|
|
|
274
305
|
// --- 索引同步 ---
|
|
@@ -370,7 +401,7 @@ class SchemaSync {
|
|
|
370
401
|
}
|
|
371
402
|
|
|
372
403
|
// --- 外键同步 ---
|
|
373
|
-
static async syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx) {
|
|
404
|
+
static async syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options) {
|
|
374
405
|
// 1. 收集定义中的外键
|
|
375
406
|
// 格式: { fkName: "xxx", createSql: "..." }
|
|
376
407
|
let targetFKs = new Map();
|
|
@@ -382,27 +413,32 @@ class SchemaSync {
|
|
|
382
413
|
|
|
383
414
|
// 解析 ref: "ModelName:colName"
|
|
384
415
|
const [refModelName, refColName] = this._parseRef(col.ref, k);
|
|
416
|
+
|
|
417
|
+
// 查找被引用的 Model 定义
|
|
418
|
+
// 假设 ctx.registry.get 返回 { Class, def }
|
|
419
|
+
const targetModelItem = ctx.registry.get(refModelName);
|
|
385
420
|
|
|
386
|
-
// 加载目标模型 (这里需要通过 ctx (NeoPG实例) 获取其他模型)
|
|
387
|
-
// 假设 ctx.registry.get(refModelName) 能拿到
|
|
388
|
-
// 或者如果是文件路径,尝试 require
|
|
389
|
-
|
|
390
|
-
// 为了简化复刻,这里假设我们能拿到目标表名
|
|
391
|
-
// 在原逻辑中是 require 文件,这里建议 NeoPG 注册机制解决
|
|
392
|
-
// 这里做一个适配:
|
|
393
421
|
let targetTableName = '';
|
|
394
|
-
|
|
395
|
-
|
|
422
|
+
|
|
423
|
+
if (targetModelItem) {
|
|
424
|
+
targetTableName = targetModelItem.def.tableName;
|
|
425
|
+
// --- [关键] 递归同步 ---
|
|
426
|
+
// 在创建外键之前,确保目标表已经存在且结构最新
|
|
427
|
+
if (debug) console.log(`[Recursive Sync] Triggered by FK: ${def.modelName} -> ${refModelName}`);
|
|
428
|
+
|
|
429
|
+
await this.execute(sql, targetModelItem.def, ctx, options);
|
|
396
430
|
} else {
|
|
397
|
-
|
|
398
|
-
|
|
431
|
+
// 目标模型未注册,可能是直接引用的表名 (Fallback)
|
|
432
|
+
if (debug) console.warn(`[NeoPG] Referenced model '${refModelName}' not found in registry. Using as table name.`);
|
|
433
|
+
targetTableName = refModelName.toLowerCase();
|
|
399
434
|
}
|
|
400
435
|
|
|
401
|
-
//
|
|
436
|
+
// 准备外键 SQL
|
|
402
437
|
const fkName = `${def.tableName}_${k}_fkey`;
|
|
403
438
|
|
|
404
439
|
// 构建 REFERENCES 子句
|
|
405
440
|
let refSql = `REFERENCES ${schema}.${targetTableName} (${refColName})`;
|
|
441
|
+
|
|
406
442
|
if (col.refActionUpdate) refSql += ` ON UPDATE ${col.refActionUpdate}`;
|
|
407
443
|
else refSql += ` ON UPDATE CASCADE`; // 默认
|
|
408
444
|
|
|
@@ -497,14 +533,15 @@ class SchemaSync {
|
|
|
497
533
|
|
|
498
534
|
static _parseRef(refstr, curColumn) {
|
|
499
535
|
if (refstr.includes(':')) {
|
|
500
|
-
const parts = refstr.split(':')
|
|
536
|
+
const parts = refstr.split(':')
|
|
501
537
|
// 处理 Model:col 格式,取最后一部分做 col,前面做 Model
|
|
502
|
-
const col = parts.pop()
|
|
503
|
-
const model = parts.join(':')
|
|
504
|
-
return [model, col]
|
|
538
|
+
const col = parts.pop()
|
|
539
|
+
const model = parts.join(':')
|
|
540
|
+
return [model, col]
|
|
505
541
|
}
|
|
506
|
-
|
|
542
|
+
|
|
543
|
+
return [refstr, curColumn]
|
|
507
544
|
}
|
|
508
545
|
}
|
|
509
546
|
|
|
510
|
-
module.exports = SchemaSync
|
|
547
|
+
module.exports = SchemaSync
|
package/lib/TransactionScope.js
CHANGED
package/package.json
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neopg",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "orm for postgres",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"neopg",
|
|
7
|
+
"NeoPG",
|
|
8
|
+
"pg",
|
|
9
|
+
"neo",
|
|
10
|
+
"pgorm",
|
|
11
|
+
"psqlorm",
|
|
6
12
|
"postgres",
|
|
7
13
|
"orm",
|
|
8
|
-
"database"
|
|
14
|
+
"database",
|
|
15
|
+
"postgres.js"
|
|
9
16
|
],
|
|
10
17
|
"homepage": "https://github.com/master-genius/neopg#readme",
|
|
11
18
|
"bugs": {
|
|
12
19
|
"url": "https://github.com/master-genius/neopg/issues"
|
|
13
20
|
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"neopg-model": "./bin/neopg-model.js"
|
|
23
|
+
},
|
|
14
24
|
"repository": {
|
|
15
25
|
"type": "git",
|
|
16
26
|
"url": "git+https://github.com/master-genius/neopg.git"
|