neopg 2.0.6 → 2.0.7
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 +2 -2
- package/lib/NeoPG.js +2 -3
- package/lib/SchemaSync.js +62 -105
- package/package.json +1 -1
- package/test/test-db.js +4 -2
package/lib/ModelChain.js
CHANGED
package/lib/NeoPG.js
CHANGED
|
@@ -25,7 +25,6 @@ function toPascalCase(str) {
|
|
|
25
25
|
class NeoPG {
|
|
26
26
|
constructor(config) {
|
|
27
27
|
this.driver = postgres(config)
|
|
28
|
-
this.ModelChain = ModelChain
|
|
29
28
|
this.sql = this.driver
|
|
30
29
|
|
|
31
30
|
this.defaultSchema = config.schema || 'public'
|
|
@@ -35,7 +34,7 @@ class NeoPG {
|
|
|
35
34
|
|
|
36
35
|
table(tableName, schema = null) {
|
|
37
36
|
const target = schema || this.defaultSchema
|
|
38
|
-
let m = new
|
|
37
|
+
let m = new ModelChain(this, {tableName, isRaw: true}, target)
|
|
39
38
|
m._isRaw = true
|
|
40
39
|
return m
|
|
41
40
|
}
|
|
@@ -65,7 +64,7 @@ class NeoPG {
|
|
|
65
64
|
if (typeof input === 'function') {
|
|
66
65
|
ModelClass = input
|
|
67
66
|
} else {
|
|
68
|
-
ModelClass =
|
|
67
|
+
ModelClass = ModelChain.from(input)
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
const rawSchema = ModelClass.schema
|
package/lib/SchemaSync.js
CHANGED
|
@@ -28,7 +28,7 @@ const DataTypeMap = {
|
|
|
28
28
|
const Numerics = ['smallint','bigint','integer','decimal','numeric', 'int'];
|
|
29
29
|
const Strings = ['char', 'varchar', 'text'];
|
|
30
30
|
const TypeWithBrackets = ['character varying', 'character', 'decimal', 'numeric'];
|
|
31
|
-
const DefaultWithType = ['varchar', 'char', 'text', 'bytea', 'timestamp', '
|
|
31
|
+
const DefaultWithType = ['varchar', 'char', 'text', 'bytea', 'timestamp', 'timestamptz', 'date', 'time'];
|
|
32
32
|
|
|
33
33
|
class SchemaSync {
|
|
34
34
|
|
|
@@ -47,29 +47,26 @@ class SchemaSync {
|
|
|
47
47
|
const tableName = def.tableName;
|
|
48
48
|
const curTableName = `${schema}.${tableName}`;
|
|
49
49
|
|
|
50
|
-
// [递归锁初始化]
|
|
50
|
+
// [递归锁初始化]
|
|
51
51
|
if (!options.syncedModels) {
|
|
52
52
|
options.syncedModels = new Set();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// 如果该表在本次递归链中已经同步过,则跳过
|
|
56
55
|
if (options.syncedModels.has(def.modelName)) {
|
|
57
56
|
return;
|
|
58
57
|
}
|
|
59
|
-
// 标记当前表已开始同步
|
|
60
58
|
options.syncedModels.add(def.modelName);
|
|
61
59
|
|
|
62
60
|
if (debug) console.log(`检测数据表 ${tableName} 的column...`);
|
|
63
61
|
|
|
64
|
-
// 0. 检查列定义的合法性
|
|
65
|
-
// 简单复刻:NeoPG 的 ModelDef 已经做了一部分,这里补充检查
|
|
62
|
+
// 0. 检查列定义的合法性
|
|
66
63
|
for (let k in def.columns) {
|
|
67
64
|
if (k.toLowerCase() !== k) {
|
|
68
65
|
console.warn(`[NeoPG Warning] ${tableName} column ${k} 包含大写,Postgres会转小写,建议代码中改为小写。`);
|
|
69
66
|
}
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
// 1. 获取 Schema OID
|
|
69
|
+
// 1. 获取 Schema OID
|
|
73
70
|
let schemaOid = null;
|
|
74
71
|
const nsRes = await sql`SELECT oid FROM pg_namespace WHERE nspname = ${schema}`;
|
|
75
72
|
if (nsRes.length > 0) schemaOid = nsRes[0].oid;
|
|
@@ -85,7 +82,6 @@ class SchemaSync {
|
|
|
85
82
|
// A. 表不存在 -> 创建
|
|
86
83
|
if (tableInfo.length === 0) {
|
|
87
84
|
await this.createTable(sql, def, schema, curTableName, debug);
|
|
88
|
-
// 同步索引、约束、外键
|
|
89
85
|
await this.syncIndex(sql, def, schema, curTableName, debug);
|
|
90
86
|
await this.syncUnique(sql, def, schema, curTableName, debug);
|
|
91
87
|
await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options);
|
|
@@ -93,7 +89,6 @@ class SchemaSync {
|
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
// B. 表存在 -> 增量同步
|
|
96
|
-
// 获取现有列信息
|
|
97
92
|
const cols = await sql`
|
|
98
93
|
SELECT column_name, data_type, column_default, character_maximum_length,
|
|
99
94
|
numeric_precision, numeric_scale, is_nullable, is_generated
|
|
@@ -106,11 +101,6 @@ class SchemaSync {
|
|
|
106
101
|
const inf = {};
|
|
107
102
|
for (const c of cols) inf[c.column_name] = c;
|
|
108
103
|
|
|
109
|
-
// 预处理 rename 逻辑需要
|
|
110
|
-
// 若存在 dropIndex 但是不存在 removeIndex 则指向 dropIndex (原逻辑)
|
|
111
|
-
// NeoPG 中这部分配置可能需要从 def 传递进来,假设 def.rawSchema 包含这些配置
|
|
112
|
-
// 为了简化,我们假设 def 上挂载了 extra 属性用于存储 index/unique 等非 column 配置
|
|
113
|
-
|
|
114
104
|
await this.syncColumn(sql, def, inf, curTableName, debug, force, dropNotExistCol);
|
|
115
105
|
await this.syncIndex(sql, def, schema, curTableName, debug);
|
|
116
106
|
await this.syncUnique(sql, def, schema, curTableName, debug);
|
|
@@ -120,6 +110,22 @@ class SchemaSync {
|
|
|
120
110
|
if (debug) console.log(` - 表结构同步完成 (${tableName}) - `);
|
|
121
111
|
}
|
|
122
112
|
|
|
113
|
+
// --- [NEW] 抽取出的默认值推导逻辑 ---
|
|
114
|
+
static _ensureDefaultValue(col) {
|
|
115
|
+
// 如果已经有默认值定义,则跳过
|
|
116
|
+
if (col.default !== undefined) return;
|
|
117
|
+
|
|
118
|
+
const pt = this._parseType(col.type);
|
|
119
|
+
|
|
120
|
+
if (col.type.includes('[')) {
|
|
121
|
+
col.default = '{}';
|
|
122
|
+
} else if (Numerics.includes(pt)) {
|
|
123
|
+
col.default = 0;
|
|
124
|
+
} else if (Strings.includes(pt)) {
|
|
125
|
+
col.default = '';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
// --- 创建表逻辑 ---
|
|
124
130
|
static async createTable(sql, def, schema, curTableName, debug) {
|
|
125
131
|
let colSqls = [];
|
|
@@ -136,13 +142,8 @@ class SchemaSync {
|
|
|
136
142
|
} else {
|
|
137
143
|
if (col.notNull !== false) line += ' not null';
|
|
138
144
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
if (col.default === undefined) {
|
|
142
|
-
if (col.type.includes('[')) col.default = '{}';
|
|
143
|
-
else if (Numerics.includes(pt)) col.default = 0;
|
|
144
|
-
else if (Strings.includes(pt)) col.default = '';
|
|
145
|
-
}
|
|
145
|
+
// [Modified] 使用统一方法推导默认值
|
|
146
|
+
this._ensureDefaultValue(col);
|
|
146
147
|
|
|
147
148
|
if (col.default !== undefined) {
|
|
148
149
|
if (col.default === null) line += ' default null';
|
|
@@ -152,7 +153,6 @@ class SchemaSync {
|
|
|
152
153
|
colSqls.push(line);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
// 联合主键
|
|
156
156
|
if (Array.isArray(def.primaryKey)) {
|
|
157
157
|
colSqls.push(`primary key (${def.primaryKey.join(',')})`);
|
|
158
158
|
}
|
|
@@ -162,7 +162,7 @@ class SchemaSync {
|
|
|
162
162
|
await sql.unsafe(createSql);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
// --- 列同步逻辑
|
|
165
|
+
// --- 列同步逻辑 ---
|
|
166
166
|
static async syncColumn(sql, def, inf, curTableName, debug, force, dropNotExistCol) {
|
|
167
167
|
const qtag = randstring(12);
|
|
168
168
|
let renameTable = {};
|
|
@@ -184,7 +184,7 @@ class SchemaSync {
|
|
|
184
184
|
const oldName = col.oldName.trim();
|
|
185
185
|
if (inf[k] === undefined && inf[oldName]) {
|
|
186
186
|
await sql.unsafe(`ALTER TABLE ${curTableName} RENAME ${this.fmtColName(oldName)} TO ${this.fmtColName(k)}`);
|
|
187
|
-
inf[k] = inf[oldName];
|
|
187
|
+
inf[k] = inf[oldName];
|
|
188
188
|
renameTable[oldName] = true;
|
|
189
189
|
}
|
|
190
190
|
}
|
|
@@ -197,12 +197,8 @@ class SchemaSync {
|
|
|
197
197
|
let addSql = `ALTER TABLE ${curTableName} ADD COLUMN ${this.fmtColName(k)} ${col.type}`;
|
|
198
198
|
if (col.notNull !== false) addSql += ' not null';
|
|
199
199
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
if (col.type.includes('[')) col.default = '{}';
|
|
203
|
-
else if (Numerics.includes(pt)) col.default = 0;
|
|
204
|
-
else if (Strings.includes(pt)) col.default = '';
|
|
205
|
-
}
|
|
200
|
+
// [Modified] 使用统一方法推导默认值
|
|
201
|
+
this._ensureDefaultValue(col);
|
|
206
202
|
|
|
207
203
|
if (col.default !== undefined) {
|
|
208
204
|
if (col.default === null) addSql += ' default null';
|
|
@@ -215,13 +211,12 @@ class SchemaSync {
|
|
|
215
211
|
}
|
|
216
212
|
|
|
217
213
|
if (col.typeLock) continue;
|
|
218
|
-
if (real_type === null) continue;
|
|
214
|
+
if (real_type === null) continue;
|
|
219
215
|
|
|
220
216
|
// 4. Check Type Change
|
|
221
217
|
if (this._compareType(inf[k], col, real_type) === false) {
|
|
222
218
|
let alterSql = `ALTER TABLE ${curTableName} ALTER COLUMN ${this.fmtColName(k)} TYPE ${col.type}`;
|
|
223
219
|
|
|
224
|
-
// 特殊处理字符串转非字符串的问题
|
|
225
220
|
const isDbString = inf[k].data_type === 'text' || inf[k].data_type.includes('character');
|
|
226
221
|
const isTargetString = Strings.includes(this._parseType(col.type));
|
|
227
222
|
|
|
@@ -229,11 +224,20 @@ class SchemaSync {
|
|
|
229
224
|
if (col.force) {
|
|
230
225
|
// 强制重建列
|
|
231
226
|
await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(k)}`);
|
|
227
|
+
|
|
232
228
|
let reAddSql = `ALTER TABLE ${curTableName} ADD COLUMN ${this.fmtColName(k)} ${col.type}`;
|
|
233
229
|
if (col.notNull !== false) reAddSql += ' not null';
|
|
234
|
-
|
|
230
|
+
|
|
231
|
+
// [Modified] 强制重建时,必须先推导默认值,否则 not null 会导致已有数据报错
|
|
232
|
+
this._ensureDefaultValue(col);
|
|
233
|
+
|
|
234
|
+
if (col.default !== undefined) {
|
|
235
|
+
if (col.default === null) reAddSql += ' default null';
|
|
236
|
+
else reAddSql += ` default $_${qtag}_$${col.default}$_${qtag}_$`;
|
|
237
|
+
}
|
|
238
|
+
|
|
235
239
|
await sql.unsafe(reAddSql);
|
|
236
|
-
col.changed = true;
|
|
240
|
+
col.changed = true;
|
|
237
241
|
continue;
|
|
238
242
|
} else {
|
|
239
243
|
console.error(`Error: ${k} 从字符串转向其他类型无转换规则,且未设置force选项。`);
|
|
@@ -251,13 +255,9 @@ class SchemaSync {
|
|
|
251
255
|
}
|
|
252
256
|
}
|
|
253
257
|
|
|
254
|
-
// 5. Check Default Value
|
|
258
|
+
// 5. Check Default Value (Only alter if explicitly changed or added)
|
|
255
259
|
if (col.default !== undefined) {
|
|
256
|
-
// 简单比对逻辑 (注:PG存储的默认值格式可能不同,这里仅作简单触发)
|
|
257
|
-
// 实际生产中可能需要更复杂的解析,这里保留原逻辑结构
|
|
258
|
-
// 原逻辑用了 _realDefault 方法,这里我们简单处理,仅当需要时设置
|
|
259
260
|
let default_val_sql = col.default === null ? 'null' : `$_${qtag}_$${col.default}$_${qtag}_$`;
|
|
260
|
-
// 这里为了简化,每次都重设默认值(开销很小),或者你需要实现 _realDefault
|
|
261
261
|
await sql.unsafe(`ALTER TABLE ${curTableName} ALTER COLUMN ${this.fmtColName(k)} SET DEFAULT ${default_val_sql}`);
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -266,67 +266,43 @@ class SchemaSync {
|
|
|
266
266
|
if (inf[k].is_nullable === 'YES') {
|
|
267
267
|
await sql.unsafe(`ALTER TABLE ${curTableName} ALTER COLUMN ${this.fmtColName(k)} SET NOT NULL`);
|
|
268
268
|
}
|
|
269
|
-
} else {
|
|
270
|
-
if (inf[k].is_nullable === 'NO') {
|
|
271
|
-
// 难以恢复为 Nullable,跳过
|
|
272
|
-
}
|
|
273
269
|
}
|
|
274
270
|
}
|
|
275
271
|
|
|
276
272
|
// 7. Drop Not Exist (Force Mode)
|
|
277
273
|
if (dropNotExistCol) {
|
|
278
274
|
for (let dbColName in inf) {
|
|
279
|
-
// 如果代码中没定义,且不是刚改名的
|
|
280
275
|
if (!def.columns[dbColName] && !renameTable[dbColName]) {
|
|
281
|
-
|
|
282
276
|
const dbCol = inf[dbColName];
|
|
283
|
-
|
|
284
|
-
// [核心逻辑] 如果是数据库层面的虚拟列 (is_generated == 'ALWAYS'),豁免,不删除
|
|
285
277
|
if (dbCol.is_generated === 'ALWAYS') {
|
|
286
278
|
if (debug) console.log(`[NeoPG] Ignoring DB-only generated column: ${dbColName}`);
|
|
287
279
|
continue;
|
|
288
280
|
}
|
|
289
|
-
|
|
290
|
-
// [额外建议] 如果是 identity 列 (自增列的一种新形式),通常也建议豁免,防止误删
|
|
291
|
-
// if (dbCol.is_identity === 'YES') continue;
|
|
292
|
-
|
|
293
281
|
if (debug) console.log(`Deleting unused column: ${dbColName}`);
|
|
294
282
|
await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(dbColName)}`);
|
|
295
283
|
}
|
|
296
284
|
}
|
|
297
285
|
}
|
|
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
|
-
} */
|
|
303
286
|
}
|
|
304
287
|
|
|
305
288
|
// --- 索引同步 ---
|
|
306
289
|
static async syncIndex(sql, def, schema, curTableName, debug) {
|
|
307
|
-
// 假设索引定义在 def.rawSchema.index (数组)
|
|
308
|
-
// ModelDef 需要暴露这个属性,或 def.indices
|
|
309
290
|
const indices = def.index || [];
|
|
310
291
|
if (!Array.isArray(indices)) return;
|
|
311
292
|
|
|
312
293
|
for (const indname of indices) {
|
|
313
|
-
// 检查 removeIndex 配置
|
|
314
294
|
const removeIndex = def.removeIndex || [];
|
|
315
295
|
if (removeIndex.includes(indname)) continue;
|
|
316
296
|
|
|
317
|
-
// 检查列是否存在
|
|
318
297
|
if (!this._checkColumnsExist(indname, def)) {
|
|
319
298
|
console.error(`Index ${indname} 包含不存在的列,跳过。`);
|
|
320
299
|
continue;
|
|
321
300
|
}
|
|
322
301
|
|
|
323
|
-
// 检查索引是否存在
|
|
324
302
|
const idxCols = indname.split(',').map(s=>s.trim()).filter(s=>s);
|
|
325
303
|
const idxNamePart = idxCols.join('_');
|
|
326
304
|
const targetIdxName = `${def.tableName}_${idxNamePart}_idx`;
|
|
327
305
|
|
|
328
|
-
// 使用 pg_indexes 查询
|
|
329
|
-
// 注意:pg_indexes 不支持 unsafe 拼 schema,只能查 schemaname 列
|
|
330
306
|
const exist = await sql`
|
|
331
307
|
SELECT * FROM pg_indexes
|
|
332
308
|
WHERE tablename = ${def.tableName}
|
|
@@ -335,11 +311,6 @@ class SchemaSync {
|
|
|
335
311
|
`;
|
|
336
312
|
|
|
337
313
|
if (exist.length > 0) continue;
|
|
338
|
-
|
|
339
|
-
// 创建索引
|
|
340
|
-
// 支持 using gin 等 (这里简化处理,假设无特殊 using)
|
|
341
|
-
// 你的原代码有 indexType 检测,这里简单复刻
|
|
342
|
-
// let ind_using = ...
|
|
343
314
|
await sql.unsafe(`CREATE INDEX ON ${curTableName} (${idxCols.map(c=>this.fmtColName(c)).join(',')})`);
|
|
344
315
|
}
|
|
345
316
|
}
|
|
@@ -348,12 +319,29 @@ class SchemaSync {
|
|
|
348
319
|
const uniques = def.unique || [];
|
|
349
320
|
if (!Array.isArray(uniques)) return;
|
|
350
321
|
|
|
322
|
+
const pkSet = new Set();
|
|
323
|
+
if (Array.isArray(def.primaryKey)) {
|
|
324
|
+
def.primaryKey.forEach(k => pkSet.add(k));
|
|
325
|
+
} else if (def.primaryKey) {
|
|
326
|
+
pkSet.add(def.primaryKey);
|
|
327
|
+
}
|
|
328
|
+
|
|
351
329
|
for (const indname of uniques) {
|
|
352
330
|
if (!this._checkColumnsExist(indname, def)) continue;
|
|
353
331
|
|
|
354
332
|
const idxCols = indname.split(',').map(s=>s.trim()).filter(s=>s);
|
|
333
|
+
|
|
334
|
+
// 监测是否等于主键
|
|
335
|
+
if (pkSet.size > 0 && idxCols.length === pkSet.size) {
|
|
336
|
+
const isPk = idxCols.every(col => pkSet.has(col));
|
|
337
|
+
if (isPk) {
|
|
338
|
+
if (debug) console.log(`[NeoPG] Unique '${indname}' matches Primary Key. Skipping.`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
355
343
|
const idxNamePart = idxCols.join('_');
|
|
356
|
-
const targetIdxName = `${def.tableName}_${idxNamePart}_idx`;
|
|
344
|
+
const targetIdxName = `${def.tableName}_${idxNamePart}_idx`;
|
|
357
345
|
|
|
358
346
|
const exist = await sql`
|
|
359
347
|
SELECT * FROM pg_indexes
|
|
@@ -369,7 +357,6 @@ class SchemaSync {
|
|
|
369
357
|
}
|
|
370
358
|
|
|
371
359
|
static async autoRemoveIndex(sql, def, schema, tableName, debug) {
|
|
372
|
-
// 1. 获取当前所有索引
|
|
373
360
|
const allIdx = await sql`
|
|
374
361
|
SELECT indexname FROM pg_indexes
|
|
375
362
|
WHERE tablename = ${tableName}
|
|
@@ -381,7 +368,6 @@ class SchemaSync {
|
|
|
381
368
|
|
|
382
369
|
const currentIdxNames = allIdx.map(i => i.indexname);
|
|
383
370
|
|
|
384
|
-
// 2. 计算应该保留的索引名
|
|
385
371
|
const indices = def.index || [];
|
|
386
372
|
const uniques = def.unique || [];
|
|
387
373
|
|
|
@@ -391,7 +377,6 @@ class SchemaSync {
|
|
|
391
377
|
indices.forEach(n => keepSet.add(makeName(n)));
|
|
392
378
|
uniques.forEach(n => keepSet.add(makeName(n)));
|
|
393
379
|
|
|
394
|
-
// 3. 差集删除
|
|
395
380
|
for (const idxName of currentIdxNames) {
|
|
396
381
|
if (!keepSet.has(idxName)) {
|
|
397
382
|
if (debug) console.log('Auto removing index:', idxName);
|
|
@@ -402,56 +387,38 @@ class SchemaSync {
|
|
|
402
387
|
|
|
403
388
|
// --- 外键同步 ---
|
|
404
389
|
static async syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options) {
|
|
405
|
-
// 1. 收集定义中的外键
|
|
406
|
-
// 格式: { fkName: "xxx", createSql: "..." }
|
|
407
390
|
let targetFKs = new Map();
|
|
408
|
-
|
|
409
|
-
|
|
391
|
+
|
|
410
392
|
for (let k in def.columns) {
|
|
411
393
|
const col = def.columns[k];
|
|
412
394
|
if (!col.ref) continue;
|
|
413
395
|
|
|
414
|
-
// 解析 ref: "ModelName:colName"
|
|
415
396
|
const [refModelName, refColName] = this._parseRef(col.ref, k);
|
|
416
|
-
|
|
417
|
-
// 查找被引用的 Model 定义
|
|
418
|
-
// 假设 ctx.registry.get 返回 { Class, def }
|
|
419
397
|
const targetModelItem = ctx.registry.get(refModelName);
|
|
420
|
-
|
|
421
398
|
let targetTableName = '';
|
|
422
399
|
|
|
423
400
|
if (targetModelItem) {
|
|
424
401
|
targetTableName = targetModelItem.def.tableName;
|
|
425
|
-
// --- [关键] 递归同步 ---
|
|
426
|
-
// 在创建外键之前,确保目标表已经存在且结构最新
|
|
427
402
|
if (debug) console.log(`[Recursive Sync] Triggered by FK: ${def.modelName} -> ${refModelName}`);
|
|
428
|
-
|
|
429
403
|
await this.execute(sql, targetModelItem.def, ctx, options);
|
|
430
404
|
} else {
|
|
431
|
-
// 目标模型未注册,可能是直接引用的表名 (Fallback)
|
|
432
|
-
if (debug) console.warn(`[NeoPG] Referenced model '${refModelName}' not found in registry. Using as table name.`);
|
|
433
405
|
targetTableName = refModelName.toLowerCase();
|
|
434
406
|
}
|
|
435
407
|
|
|
436
|
-
// 准备外键 SQL
|
|
437
408
|
const fkName = `${def.tableName}_${k}_fkey`;
|
|
438
|
-
|
|
439
|
-
// 构建 REFERENCES 子句
|
|
440
409
|
let refSql = `REFERENCES ${schema}.${targetTableName} (${refColName})`;
|
|
441
410
|
|
|
442
411
|
if (col.refActionUpdate) refSql += ` ON UPDATE ${col.refActionUpdate}`;
|
|
443
|
-
else refSql += ` ON UPDATE CASCADE`;
|
|
412
|
+
else refSql += ` ON UPDATE CASCADE`;
|
|
444
413
|
|
|
445
414
|
if (col.refActionDelete) refSql += ` ON DELETE ${col.refActionDelete}`;
|
|
446
|
-
else refSql += ` ON DELETE CASCADE`;
|
|
415
|
+
else refSql += ` ON DELETE CASCADE`;
|
|
447
416
|
|
|
448
417
|
targetFKs.set(fkName, { col: k, sql: refSql, changed: col.changed });
|
|
449
418
|
}
|
|
450
419
|
|
|
451
|
-
// 2. 获取数据库现有外键
|
|
452
420
|
const existFKs = new Set();
|
|
453
421
|
if (targetFKs.size > 0 && schemaOid) {
|
|
454
|
-
// 构建 IN 查询
|
|
455
422
|
const names = Array.from(targetFKs.keys());
|
|
456
423
|
const rows = await sql`
|
|
457
424
|
SELECT conname FROM pg_constraint
|
|
@@ -462,9 +429,7 @@ class SchemaSync {
|
|
|
462
429
|
rows.forEach(r => existFKs.add(r.conname));
|
|
463
430
|
}
|
|
464
431
|
|
|
465
|
-
// 3. 同步
|
|
466
432
|
for (const [fkName, conf] of targetFKs) {
|
|
467
|
-
// 如果变更了列类型,必须先删后加
|
|
468
433
|
if (existFKs.has(fkName) && conf.changed) {
|
|
469
434
|
await sql.unsafe(`ALTER TABLE ${curTableName} DROP CONSTRAINT ${fkName}`);
|
|
470
435
|
existFKs.delete(fkName);
|
|
@@ -481,11 +446,9 @@ class SchemaSync {
|
|
|
481
446
|
// --- 辅助方法 ---
|
|
482
447
|
|
|
483
448
|
static fmtColName(col) {
|
|
484
|
-
// 简单处理引用
|
|
485
449
|
if (forbidColumns.quote.includes(col.toLowerCase())) {
|
|
486
450
|
return `"${col}"`;
|
|
487
451
|
}
|
|
488
|
-
|
|
489
452
|
return `"${col}"`;
|
|
490
453
|
}
|
|
491
454
|
|
|
@@ -502,8 +465,6 @@ class SchemaSync {
|
|
|
502
465
|
return f.data_type === real_type;
|
|
503
466
|
}
|
|
504
467
|
|
|
505
|
-
// 括号解析
|
|
506
|
-
// 原逻辑 _parseBrackets
|
|
507
468
|
const idx = col.type.indexOf('(');
|
|
508
469
|
const brackets = idx > 0 ? col.type.substring(idx).trim() : '';
|
|
509
470
|
|
|
@@ -511,16 +472,14 @@ class SchemaSync {
|
|
|
511
472
|
return `${f.data_type}(${f.character_maximum_length})` === `${real_type}${brackets}`;
|
|
512
473
|
}
|
|
513
474
|
|
|
514
|
-
// numeric(p,s)
|
|
515
475
|
if (f.data_type === 'numeric' || f.data_type === 'decimal') {
|
|
516
|
-
// 注意 PG 返回的 precision 可能是 null
|
|
517
476
|
const p = f.numeric_precision;
|
|
518
477
|
const s = f.numeric_scale;
|
|
519
|
-
if (!p) return `${real_type}${brackets}` === real_type;
|
|
478
|
+
if (!p) return `${real_type}${brackets}` === real_type;
|
|
520
479
|
return `${f.data_type}(${p},${s})` === `${real_type}${brackets}`;
|
|
521
480
|
}
|
|
522
481
|
|
|
523
|
-
return false;
|
|
482
|
+
return false;
|
|
524
483
|
}
|
|
525
484
|
|
|
526
485
|
static _checkColumnsExist(colsStr, def) {
|
|
@@ -534,12 +493,10 @@ class SchemaSync {
|
|
|
534
493
|
static _parseRef(refstr, curColumn) {
|
|
535
494
|
if (refstr.includes(':')) {
|
|
536
495
|
const parts = refstr.split(':')
|
|
537
|
-
// 处理 Model:col 格式,取最后一部分做 col,前面做 Model
|
|
538
496
|
const col = parts.pop()
|
|
539
497
|
const model = parts.join(':')
|
|
540
498
|
return [model, col]
|
|
541
499
|
}
|
|
542
|
-
|
|
543
500
|
return [refstr, curColumn]
|
|
544
501
|
}
|
|
545
502
|
}
|
package/package.json
CHANGED
package/test/test-db.js
CHANGED
|
@@ -29,7 +29,9 @@ const User = {
|
|
|
29
29
|
},
|
|
30
30
|
|
|
31
31
|
mobile: {
|
|
32
|
-
type: dataTypes.STRING(16)
|
|
32
|
+
type: dataTypes.STRING(16),
|
|
33
|
+
//type: dataTypes.BIGINT,
|
|
34
|
+
force: true
|
|
33
35
|
},
|
|
34
36
|
|
|
35
37
|
mobile_state: {
|
|
@@ -133,7 +135,7 @@ const User = {
|
|
|
133
135
|
//唯一索引
|
|
134
136
|
unique: [
|
|
135
137
|
'username',
|
|
136
|
-
'email'
|
|
138
|
+
'email', 'id'
|
|
137
139
|
]
|
|
138
140
|
}
|
|
139
141
|
|