oak-db 3.3.13 → 4.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.
@@ -0,0 +1,984 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readPostgreSqlSchema = readPostgreSqlSchema;
4
+ exports.inspectPostgreSqlSchema = inspectPostgreSqlSchema;
5
+ exports.buildPostgreSqlMigrationPlan = buildPostgreSqlMigrationPlan;
6
+ const crypto_1 = require("crypto");
7
+ const migration_1 = require("../migration");
8
+ const inspection_1 = require("../utils/inspection");
9
+ const indexInspection_1 = require("../utils/indexInspection");
10
+ const prepare_1 = require("./prepare");
11
+ function mapOnDeleteRule(deleteRule) {
12
+ switch ((deleteRule || '').toUpperCase()) {
13
+ case 'CASCADE':
14
+ return 'cascade';
15
+ case 'SET NULL':
16
+ return 'set null';
17
+ default:
18
+ return 'no action';
19
+ }
20
+ }
21
+ function makeWarning(code, message, table, column, sql) {
22
+ return {
23
+ code,
24
+ level: 'warning',
25
+ message,
26
+ table,
27
+ column,
28
+ sql,
29
+ };
30
+ }
31
+ function escapeStringValue(value) {
32
+ return value.replace(/'/g, "''");
33
+ }
34
+ function buildPostgreSqlTempEnumTypeName(enumTypeName) {
35
+ const suffix = `__old_${(0, crypto_1.createHash)('sha1')
36
+ .update(enumTypeName)
37
+ .digest('hex')
38
+ .slice(0, 8)}`;
39
+ const maxBaseLength = 63 - suffix.length;
40
+ return `${enumTypeName.slice(0, Math.max(1, maxBaseLength))}${suffix}`;
41
+ }
42
+ function extractTypeExpression(columnDefinition) {
43
+ const withoutName = columnDefinition.replace(/^"[^"]+"\s+/, '');
44
+ const markers = [' NOT NULL', ' UNIQUE', ' DEFAULT', ' PRIMARY KEY'];
45
+ let end = withoutName.length;
46
+ markers.forEach((marker) => {
47
+ const index = withoutName.indexOf(marker);
48
+ if (index >= 0 && index < end) {
49
+ end = index;
50
+ }
51
+ });
52
+ return withoutName.slice(0, end);
53
+ }
54
+ function translateDefaultValue(attr) {
55
+ const { default: defaultValue, type } = attr;
56
+ if (defaultValue === undefined) {
57
+ return undefined;
58
+ }
59
+ if (type === 'bool' || type === 'boolean') {
60
+ return defaultValue ? 'TRUE' : 'FALSE';
61
+ }
62
+ if (typeof defaultValue === 'number') {
63
+ return String(defaultValue);
64
+ }
65
+ if (type === 'object' || type === 'array') {
66
+ return `'${escapeStringValue(JSON.stringify(defaultValue))}'::jsonb`;
67
+ }
68
+ return `'${escapeStringValue(String(defaultValue))}'`;
69
+ }
70
+ function mapPostgreSqlIndexType(indexType) {
71
+ switch (indexType.toLowerCase()) {
72
+ case 'hash':
73
+ return 'hash';
74
+ case 'gist':
75
+ return 'spatial';
76
+ default:
77
+ return 'btree';
78
+ }
79
+ }
80
+ function createPgAdapter(translator) {
81
+ const quote = (name) => translator.quoteIdentifier(name);
82
+ const getTableName = (table, tableDef) => tableDef?.storageName || table;
83
+ const getForeignKeyName = (tableName, column) => `${tableName}_${column}_fk`;
84
+ const getOnDeleteSql = (onDelete) => {
85
+ switch (onDelete) {
86
+ case 'cascade':
87
+ return 'CASCADE';
88
+ case 'set null':
89
+ return 'SET NULL';
90
+ default:
91
+ return 'NO ACTION';
92
+ }
93
+ };
94
+ const createEnumSql = (tableName, column, attr) => {
95
+ const enumTypeName = translator.getEnumTypeName(tableName, column);
96
+ const values = attr.enumeration || [];
97
+ return `DO $$ BEGIN CREATE TYPE ${quote(enumTypeName)} AS ENUM (${values.map((value) => `'${escapeStringValue(value)}'`).join(', ')}); EXCEPTION WHEN duplicate_object THEN NULL; END $$;`;
98
+ };
99
+ const buildCreateEnumTypeSql = (enumTypeName, values) => `CREATE TYPE ${quote(enumTypeName)} AS ENUM (${values.map((value) => `'${escapeStringValue(value)}'`).join(', ')});`;
100
+ const buildColumnDefaultSql = (tableName, column, defaultValue) => defaultValue === undefined
101
+ ? `ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} DROP DEFAULT;`
102
+ : `ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} SET DEFAULT ${defaultValue};`;
103
+ const buildEnumManualAlterSql = (tableName, column, oldAttr, newAttr) => {
104
+ const oldDefault = translateDefaultValue(oldAttr);
105
+ const newDefault = translateDefaultValue(newAttr);
106
+ const oldNotNull = !!oldAttr.notNull || oldAttr.type === 'geometry';
107
+ const newNotNull = !!newAttr.notNull || newAttr.type === 'geometry';
108
+ const sqls = [];
109
+ if (oldAttr.type === 'enum' && newAttr.type === 'enum') {
110
+ const enumTypeName = translator.getEnumTypeName(tableName, column);
111
+ const tempEnumTypeName = buildPostgreSqlTempEnumTypeName(enumTypeName);
112
+ if (oldDefault !== undefined) {
113
+ sqls.push(buildColumnDefaultSql(tableName, column, undefined));
114
+ }
115
+ // 先把旧类型让位,再用目标枚举定义重建同名类型,最后把列按 text 中转回绑到新类型。
116
+ sqls.push(`ALTER TYPE ${quote(enumTypeName)} RENAME TO ${quote(tempEnumTypeName)};`);
117
+ sqls.push(buildCreateEnumTypeSql(enumTypeName, newAttr.enumeration || []));
118
+ sqls.push(`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} TYPE ${quote(enumTypeName)} USING ${quote(column)}::text::${quote(enumTypeName)};`);
119
+ sqls.push(`DROP TYPE ${quote(tempEnumTypeName)};`);
120
+ }
121
+ else {
122
+ if (newAttr.type === 'enum') {
123
+ sqls.push(buildCreateEnumTypeSql(translator.getEnumTypeName(tableName, column), newAttr.enumeration || []));
124
+ }
125
+ const oldColumnDef = translator.translateColumnDefinition(tableName, column, oldAttr);
126
+ const newColumnDef = translator.translateColumnDefinition(tableName, column, newAttr);
127
+ const oldType = extractTypeExpression(oldColumnDef);
128
+ const newType = extractTypeExpression(newColumnDef);
129
+ if (oldType !== newType) {
130
+ const usingClause = oldAttr.type === 'enum' || newAttr.type === 'enum'
131
+ ? `${quote(column)}::text::${newType}`
132
+ : `${quote(column)}::${newType}`;
133
+ if (oldDefault !== undefined && oldAttr.type === 'enum') {
134
+ sqls.push(buildColumnDefaultSql(tableName, column, undefined));
135
+ }
136
+ sqls.push(`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} TYPE ${newType} USING ${usingClause};`);
137
+ }
138
+ if (oldAttr.type === 'enum') {
139
+ sqls.push(`DROP TYPE IF EXISTS ${quote(translator.getEnumTypeName(tableName, column))};`);
140
+ }
141
+ }
142
+ // enum 迁移中经常需要先临时移除默认值;这里统一按目标定义回放默认值与空值约束,
143
+ // 保证 migration.sql 执行完后列语义与最新 schema 一致。
144
+ if (oldDefault !== newDefault || (oldAttr.type === 'enum' && oldDefault !== undefined)) {
145
+ if (newDefault !== undefined) {
146
+ sqls.push(buildColumnDefaultSql(tableName, column, newDefault));
147
+ }
148
+ }
149
+ if (oldNotNull !== newNotNull) {
150
+ sqls.push(`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} ${newNotNull ? 'SET' : 'DROP'} NOT NULL;`);
151
+ }
152
+ return sqls;
153
+ };
154
+ const buildIndexNames = (tableName, table, index) => {
155
+ if (index.config?.type === 'fulltext') {
156
+ const tsConfigs = Array.isArray(index.config?.tsConfig)
157
+ ? [...index.config.tsConfig]
158
+ : [index.config?.tsConfig || 'simple'];
159
+ return tsConfigs.map((tsConfig) => ({
160
+ physicalName: translator.getPhysicalIndexName(table, tableName, index.name, tsConfigs.length > 1 ? `_${tsConfig}` : ''),
161
+ tsConfig,
162
+ }));
163
+ }
164
+ return [{
165
+ physicalName: translator.getPhysicalIndexName(table, tableName, index.name),
166
+ tsConfig: undefined,
167
+ }];
168
+ };
169
+ const buildIndexSql = (tableName, table, index, options) => {
170
+ return buildIndexNames(tableName, table, index).map(({ physicalName, tsConfig }) => {
171
+ const indexName = options?.indexNameOverride || physicalName;
172
+ let sql = 'CREATE ';
173
+ if (options?.concurrently) {
174
+ sql += 'INDEX CONCURRENTLY ';
175
+ if (index.config?.unique) {
176
+ sql = 'CREATE UNIQUE INDEX CONCURRENTLY ';
177
+ }
178
+ }
179
+ else if (index.config?.unique) {
180
+ sql += 'UNIQUE ';
181
+ }
182
+ if (!options?.concurrently) {
183
+ sql += 'INDEX ';
184
+ }
185
+ if (index.config?.unique) {
186
+ if (!options?.concurrently) {
187
+ sql += '';
188
+ }
189
+ }
190
+ sql += `IF NOT EXISTS ${quote(indexName)} ON ${quote(tableName)} `;
191
+ if (index.config?.type === 'hash') {
192
+ sql += 'USING HASH ';
193
+ }
194
+ else if (index.config?.type === 'spatial') {
195
+ sql += 'USING GIST ';
196
+ }
197
+ else if (index.config?.type === 'fulltext') {
198
+ sql += 'USING GIN ';
199
+ }
200
+ sql += '(';
201
+ if (index.config?.type === 'fulltext') {
202
+ sql += index.attributes.map(({ name }) => `to_tsvector('${tsConfig}', COALESCE(${quote(String(name))}, ''))`).join(', ');
203
+ }
204
+ else {
205
+ sql += index.attributes.map(({ name, direction }) => {
206
+ let fragment = quote(String(name));
207
+ if (direction) {
208
+ fragment += ` ${direction}`;
209
+ }
210
+ return fragment;
211
+ }).join(', ');
212
+ }
213
+ sql += ');';
214
+ return sql;
215
+ });
216
+ };
217
+ const buildDropIndexSql = (tableName, table, index, options) => {
218
+ const originNames = index.__originNames || [];
219
+ const physicalNames = options?.indexNameOverride
220
+ ? [options.indexNameOverride]
221
+ : (originNames.length
222
+ ? originNames
223
+ : (typeof index.__originName === 'string'
224
+ ? [index.__originName]
225
+ : buildIndexNames(tableName, table, index).map(({ physicalName }) => physicalName)));
226
+ return physicalNames.map((indexName) => `DROP INDEX ${options?.concurrently ? 'CONCURRENTLY ' : ''}IF EXISTS ${quote(indexName)};`);
227
+ };
228
+ const getExpectedPhysicalIndexNames = (tableName, table, index) => buildIndexNames(tableName, table, index).map(({ physicalName }) => physicalName);
229
+ const buildAlterColumnSql = (tableName, column, oldAttr, newAttr) => {
230
+ const sqls = [];
231
+ const oldColumnDef = translator.translateColumnDefinition(tableName, column, oldAttr);
232
+ const newColumnDef = translator.translateColumnDefinition(tableName, column, newAttr);
233
+ const oldType = extractTypeExpression(oldColumnDef);
234
+ const newType = extractTypeExpression(newColumnDef);
235
+ if (oldType !== newType) {
236
+ sqls.push(`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} TYPE ${newType} USING ${quote(column)}::${newType};`);
237
+ }
238
+ const oldDefault = translateDefaultValue(oldAttr);
239
+ const newDefault = translateDefaultValue(newAttr);
240
+ if (oldDefault !== newDefault) {
241
+ if (newDefault === undefined) {
242
+ sqls.push(`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} DROP DEFAULT;`);
243
+ }
244
+ else {
245
+ sqls.push(`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} SET DEFAULT ${newDefault};`);
246
+ }
247
+ }
248
+ const oldNotNull = !!oldAttr.notNull || oldAttr.type === 'geometry';
249
+ const newNotNull = !!newAttr.notNull || newAttr.type === 'geometry';
250
+ if (oldNotNull !== newNotNull) {
251
+ sqls.push(`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(column)} ${newNotNull ? 'SET' : 'DROP'} NOT NULL;`);
252
+ }
253
+ return sqls;
254
+ };
255
+ return {
256
+ dialect: 'postgresql',
257
+ normalizeIdentifier(name) {
258
+ return name;
259
+ },
260
+ getTableName,
261
+ buildPrepareSql(currentSchema, targetSchema) {
262
+ return (0, prepare_1.buildPostgreSqlPrepareSql)(currentSchema, targetSchema);
263
+ },
264
+ buildNewTablePlan(table, tableDef) {
265
+ const tableName = getTableName(table, tableDef);
266
+ const prepareSql = Object.entries(tableDef.attributes || {}).flatMap(([column, attr]) => attr.type === 'enum'
267
+ ? [createEnumSql(tableName, column, attr)]
268
+ : []);
269
+ const sql = translator.translateCreateEntity(table, {
270
+ ifExists: 'omit',
271
+ }).filter((stmt) => !/^\s*(?:DO\s+\$\$\s+BEGIN\s+)?CREATE\s+TYPE\b/i.test(stmt));
272
+ const foreignKeys = this.getForeignKeys(table, tableDef, translator.schema);
273
+ return {
274
+ prepareSql,
275
+ forwardSql: sql,
276
+ deferredForwardSql: Object.values(foreignKeys).map((foreignKey) => `ALTER TABLE ${quote(tableName)} ADD CONSTRAINT ${quote(foreignKey.name)} FOREIGN KEY (${quote(foreignKey.column)}) REFERENCES ${quote(foreignKey.refTable)} (${quote(foreignKey.refColumn)}) ON DELETE ${getOnDeleteSql(foreignKey.onDelete)};`),
277
+ summary: {
278
+ newTables: 1,
279
+ addedIndexes: (tableDef.indexes || []).length,
280
+ addedForeignKeys: Object.keys(foreignKeys).length,
281
+ },
282
+ };
283
+ },
284
+ buildDropTablePlan(tableName) {
285
+ return {
286
+ backwardSql: [
287
+ `DROP TABLE IF EXISTS ${quote(tableName)} CASCADE;`,
288
+ ],
289
+ summary: {
290
+ removedTables: 1,
291
+ },
292
+ };
293
+ },
294
+ buildRenamePlan(tableName, from, to) {
295
+ const sql = [
296
+ `ALTER TABLE ${quote(tableName)} RENAME COLUMN ${quote(from)} TO ${quote(to)};`,
297
+ ];
298
+ return {
299
+ manualSql: sql,
300
+ warnings: [
301
+ makeWarning('rename-candidate', `字段「${from}」与「${to}」结构一致,推测为重命名,请先确认再执行`, tableName, to, sql),
302
+ ],
303
+ renameCandidates: [{
304
+ table: tableName,
305
+ from,
306
+ to,
307
+ reason: '旧字段与新字段结构完全一致',
308
+ sql,
309
+ }],
310
+ summary: {
311
+ manualActions: 1,
312
+ changedColumns: 1,
313
+ },
314
+ };
315
+ },
316
+ buildColumnPlan(tableName, column, oldAttr, newAttr, classification) {
317
+ if (!oldAttr && newAttr) {
318
+ const prepareSql = newAttr.type === 'enum'
319
+ ? [createEnumSql(tableName, column, newAttr)]
320
+ : [];
321
+ const sql = `ALTER TABLE ${quote(tableName)} ADD COLUMN ${translator.translateColumnDefinition(tableName, column, newAttr)};`;
322
+ if (classification.mode === 'manual') {
323
+ return {
324
+ prepareSql,
325
+ manualSql: [sql],
326
+ warnings: [
327
+ makeWarning('manual-column-change', classification.reason || `新增字段「${column}」需要人工确认`, tableName, column, [sql]),
328
+ ],
329
+ summary: {
330
+ addedColumns: 1,
331
+ manualActions: 1,
332
+ },
333
+ };
334
+ }
335
+ return {
336
+ prepareSql,
337
+ forwardSql: [sql],
338
+ summary: {
339
+ addedColumns: 1,
340
+ },
341
+ };
342
+ }
343
+ if (oldAttr && !newAttr) {
344
+ return {
345
+ backwardSql: [
346
+ `ALTER TABLE ${quote(tableName)} DROP COLUMN ${quote(column)};`,
347
+ ],
348
+ summary: {
349
+ removedColumns: 1,
350
+ },
351
+ };
352
+ }
353
+ if (classification.mode === 'none') {
354
+ return {};
355
+ }
356
+ if (classification.enumAppendOnly && oldAttr?.enumeration && newAttr?.enumeration) {
357
+ const enumTypeName = translator.getEnumTypeName(tableName, column);
358
+ const addedValues = newAttr.enumeration.slice(oldAttr.enumeration.length);
359
+ return {
360
+ prepareSql: addedValues.map((value) => `ALTER TYPE ${quote(enumTypeName)} ADD VALUE IF NOT EXISTS '${escapeStringValue(value)}';`),
361
+ summary: {
362
+ changedColumns: addedValues.length ? 1 : 0,
363
+ },
364
+ };
365
+ }
366
+ const sql = oldAttr && newAttr
367
+ && (oldAttr.type === 'enum' || newAttr.type === 'enum')
368
+ ? buildEnumManualAlterSql(tableName, column, oldAttr, newAttr)
369
+ : buildAlterColumnSql(tableName, column, oldAttr, newAttr);
370
+ if (classification.mode === 'manual') {
371
+ return {
372
+ manualSql: sql,
373
+ warnings: [
374
+ makeWarning(newAttr?.type === 'enum' || oldAttr?.type === 'enum' ? 'manual-enum-change' : 'manual-column-change', classification.reason || `字段「${column}」变更需要人工确认`, tableName, column, sql),
375
+ ],
376
+ summary: {
377
+ changedColumns: 1,
378
+ manualActions: 1,
379
+ },
380
+ };
381
+ }
382
+ return {
383
+ forwardSql: sql,
384
+ summary: {
385
+ changedColumns: 1,
386
+ },
387
+ };
388
+ },
389
+ buildIndexPlan(tableName, table, oldIndex, newIndex, context) {
390
+ const estimatedRows = context.tableStats?.rowCount;
391
+ const isLargeTable = !context.isNewTable
392
+ && estimatedRows !== undefined
393
+ && estimatedRows >= context.largeTableRowThreshold;
394
+ const makeLargeTableIndexWarning = (message, sql) => makeWarning('large-table-index-change', `${message}${estimatedRows !== undefined ? `(当前估算行数: ${estimatedRows})` : ''}`, tableName, undefined, sql);
395
+ if (!oldIndex && newIndex) {
396
+ const sql = buildIndexSql(tableName, table, newIndex);
397
+ if (newIndex.config?.unique && !context.isNewTable) {
398
+ return {
399
+ manualSql: sql,
400
+ warnings: [
401
+ makeWarning('manual-index-change', `索引「${newIndex.name}」是唯一索引,执行前需要确认历史数据无重复`, tableName, undefined, sql),
402
+ ],
403
+ summary: {
404
+ addedIndexes: 1,
405
+ manualActions: 1,
406
+ },
407
+ };
408
+ }
409
+ if (isLargeTable) {
410
+ const onlineSql = buildIndexSql(tableName, table, newIndex, {
411
+ concurrently: true,
412
+ });
413
+ return {
414
+ onlineSql,
415
+ warnings: [
416
+ makeLargeTableIndexWarning(`大表新增索引「${newIndex.name}」已切换为在线创建`, onlineSql),
417
+ ],
418
+ summary: {
419
+ addedIndexes: 1,
420
+ onlineActions: 1,
421
+ largeTableIndexes: 1,
422
+ },
423
+ };
424
+ }
425
+ return {
426
+ forwardSql: sql,
427
+ summary: {
428
+ addedIndexes: 1,
429
+ },
430
+ };
431
+ }
432
+ if (oldIndex && !newIndex) {
433
+ if (isLargeTable && !oldIndex.config?.unique) {
434
+ const onlineSql = buildDropIndexSql(tableName, table, oldIndex, {
435
+ concurrently: true,
436
+ });
437
+ return {
438
+ onlineSql,
439
+ warnings: [
440
+ makeLargeTableIndexWarning(`大表删除索引「${oldIndex.name}」已切换为在线删除`, onlineSql),
441
+ ],
442
+ summary: {
443
+ removedIndexes: 1,
444
+ onlineActions: 1,
445
+ largeTableIndexes: 1,
446
+ },
447
+ };
448
+ }
449
+ return {
450
+ backwardSql: buildDropIndexSql(tableName, table, oldIndex),
451
+ summary: {
452
+ removedIndexes: 1,
453
+ },
454
+ };
455
+ }
456
+ const dropSql = buildDropIndexSql(tableName, table, oldIndex);
457
+ const createSql = buildIndexSql(tableName, table, newIndex);
458
+ if (isLargeTable) {
459
+ const manualSql = [
460
+ ...buildDropIndexSql(tableName, table, oldIndex, {
461
+ concurrently: true,
462
+ }),
463
+ ...buildIndexSql(tableName, table, newIndex, {
464
+ concurrently: true,
465
+ }),
466
+ ];
467
+ return {
468
+ manualSql,
469
+ warnings: [
470
+ makeLargeTableIndexWarning(`大表重建索引「${oldIndex.name}」可能导致长时间索引维护,已转为人工确认`, manualSql),
471
+ ],
472
+ summary: {
473
+ changedIndexes: 1,
474
+ manualActions: 1,
475
+ largeTableIndexes: 1,
476
+ },
477
+ };
478
+ }
479
+ if (oldIndex?.config?.unique || newIndex?.config?.unique) {
480
+ return {
481
+ manualSql: [...dropSql, ...createSql],
482
+ warnings: [
483
+ makeWarning('manual-index-change', `索引「${oldIndex.name}」包含唯一性变化,需要确认历史数据后再重建`, tableName, undefined, [...dropSql, ...createSql]),
484
+ ],
485
+ summary: {
486
+ changedIndexes: 1,
487
+ manualActions: 1,
488
+ },
489
+ };
490
+ }
491
+ return {
492
+ forwardSql: [...dropSql, ...createSql],
493
+ summary: {
494
+ changedIndexes: 1,
495
+ },
496
+ };
497
+ },
498
+ buildRenameIndexPlan(tableName, table, oldIndex, newIndex) {
499
+ const oldPhysicalNames = (0, indexInspection_1.getIndexOriginNames)(oldIndex);
500
+ const newPhysicalNames = getExpectedPhysicalIndexNames(tableName, table, newIndex);
501
+ if (oldPhysicalNames.length !== 1 || newPhysicalNames.length !== 1) {
502
+ return {};
503
+ }
504
+ if (oldPhysicalNames[0] === newPhysicalNames[0]) {
505
+ return {};
506
+ }
507
+ return {
508
+ forwardSql: [
509
+ `ALTER INDEX ${quote(oldPhysicalNames[0])} RENAME TO ${quote(newPhysicalNames[0])};`,
510
+ ],
511
+ backwardSql: [
512
+ `ALTER INDEX ${quote(newPhysicalNames[0])} RENAME TO ${quote(oldPhysicalNames[0])};`,
513
+ ],
514
+ summary: {
515
+ changedIndexes: 1,
516
+ },
517
+ };
518
+ },
519
+ buildForeignKeyPlan(tableName, oldForeignKey, newForeignKey, isNewTable) {
520
+ if (!oldForeignKey && newForeignKey) {
521
+ const sql = `ALTER TABLE ${quote(tableName)} ADD CONSTRAINT ${quote(newForeignKey.name)} FOREIGN KEY (${quote(newForeignKey.column)}) REFERENCES ${quote(newForeignKey.refTable)} (${quote(newForeignKey.refColumn)}) ON DELETE ${getOnDeleteSql(newForeignKey.onDelete)};`;
522
+ if (isNewTable) {
523
+ return {
524
+ forwardSql: [sql],
525
+ summary: {
526
+ addedForeignKeys: 1,
527
+ },
528
+ };
529
+ }
530
+ return {
531
+ manualSql: [sql],
532
+ warnings: [
533
+ makeWarning('manual-foreign-key-change', `新增外键「${newForeignKey.name}」前需要确认历史数据引用完整`, tableName, newForeignKey.column, [sql]),
534
+ ],
535
+ summary: {
536
+ addedForeignKeys: 1,
537
+ manualActions: 1,
538
+ },
539
+ };
540
+ }
541
+ if (oldForeignKey && !newForeignKey) {
542
+ return {
543
+ backwardSql: [
544
+ `ALTER TABLE ${quote(tableName)} DROP CONSTRAINT IF EXISTS ${quote(oldForeignKey.name)};`,
545
+ ],
546
+ summary: {
547
+ removedForeignKeys: 1,
548
+ },
549
+ };
550
+ }
551
+ const dropSql = `ALTER TABLE ${quote(tableName)} DROP CONSTRAINT IF EXISTS ${quote(oldForeignKey.name)};`;
552
+ const createSql = `ALTER TABLE ${quote(tableName)} ADD CONSTRAINT ${quote(newForeignKey.name)} FOREIGN KEY (${quote(newForeignKey.column)}) REFERENCES ${quote(newForeignKey.refTable)} (${quote(newForeignKey.refColumn)}) ON DELETE ${getOnDeleteSql(newForeignKey.onDelete)};`;
553
+ return {
554
+ manualSql: [dropSql, createSql],
555
+ warnings: [
556
+ makeWarning('manual-foreign-key-change', `外键「${oldForeignKey.name}」发生变化,需要确认数据后再重建`, tableName, newForeignKey.column, [dropSql, createSql]),
557
+ ],
558
+ summary: {
559
+ changedForeignKeys: 1,
560
+ manualActions: 1,
561
+ },
562
+ };
563
+ },
564
+ getForeignKeys(table, tableDef, fullSchema) {
565
+ const tableName = getTableName(table, tableDef);
566
+ const foreignKeys = {};
567
+ Object.keys(tableDef.attributes || {}).forEach((column) => {
568
+ const attr = tableDef.attributes[column];
569
+ if (attr.type !== 'ref' || Array.isArray(attr.ref) || !attr.ref) {
570
+ return;
571
+ }
572
+ const refTableDef = fullSchema[attr.ref];
573
+ const refTable = refTableDef ? getTableName(attr.ref, refTableDef) : attr.ref;
574
+ const foreignKey = {
575
+ name: getForeignKeyName(tableName, column),
576
+ column,
577
+ refTable,
578
+ refColumn: 'id',
579
+ onDelete: attr.onRefDelete === 'delete'
580
+ ? 'cascade'
581
+ : (attr.onRefDelete === 'setNull' ? 'set null' : 'no action'),
582
+ };
583
+ foreignKeys[foreignKey.name] = foreignKey;
584
+ });
585
+ return foreignKeys;
586
+ },
587
+ };
588
+ }
589
+ function parseDefaultValue(value, attr) {
590
+ if (value === null || value === undefined) {
591
+ return undefined;
592
+ }
593
+ if (typeof value === 'string') {
594
+ let trimmed = value.trim();
595
+ const stringLiteralMatch = trimmed.match(/^(?:E)?'((?:''|[^'])*)'(?:\s*::[\w\s".\[\]]+)?$/s);
596
+ if (stringLiteralMatch) {
597
+ trimmed = stringLiteralMatch[1];
598
+ }
599
+ else {
600
+ const castPattern = /\s*::[\w\s".\[\]]+$/;
601
+ while (castPattern.test(trimmed)) {
602
+ trimmed = trimmed.replace(castPattern, '').trim();
603
+ }
604
+ while (trimmed.startsWith('(') && trimmed.endsWith(')')) {
605
+ trimmed = trimmed.slice(1, -1).trim();
606
+ }
607
+ if (/^e'[\s\S]*'$/i.test(trimmed)) {
608
+ trimmed = trimmed.slice(2, -1);
609
+ }
610
+ else if (/^'(.*)'$/s.test(trimmed)) {
611
+ trimmed = trimmed.slice(1, -1);
612
+ }
613
+ }
614
+ trimmed = trimmed.replace(/''/g, "'");
615
+ if (trimmed === '') {
616
+ return '';
617
+ }
618
+ if (trimmed === 'true') {
619
+ return true;
620
+ }
621
+ if (trimmed === 'false') {
622
+ return false;
623
+ }
624
+ if ((attr.type === 'object' || attr.type === 'array')
625
+ && /^[\[{]/.test(trimmed)) {
626
+ try {
627
+ return JSON.parse(trimmed);
628
+ }
629
+ catch (err) {
630
+ }
631
+ }
632
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
633
+ return Number(trimmed);
634
+ }
635
+ return trimmed;
636
+ }
637
+ if (typeof value === 'boolean' || typeof value === 'number') {
638
+ return value;
639
+ }
640
+ return String(value);
641
+ }
642
+ function getFullTextSuffixes(index) {
643
+ if (index.config?.type !== 'fulltext') {
644
+ return [''];
645
+ }
646
+ const tsConfigs = Array.isArray(index.config?.tsConfig)
647
+ ? index.config.tsConfig
648
+ : [index.config?.tsConfig || 'simple'];
649
+ if (tsConfigs.length <= 1) {
650
+ return [''];
651
+ }
652
+ return tsConfigs.map((tsConfig) => `_${tsConfig}`);
653
+ }
654
+ function findMatchingPostgreSqlIndexHint(translator, entityName, tableName, actualIndexName, indexes) {
655
+ return (indexes || []).find((candidate) => getFullTextSuffixes(candidate).some((suffix) => translator.getLegacyPhysicalIndexNames(entityName, tableName, candidate.name, suffix).includes(actualIndexName)));
656
+ }
657
+ const postgreSqlAttributeHintRules = [
658
+ {
659
+ actualTypes: ['bigint'],
660
+ semanticTypes: ['date', 'time', 'datetime', 'money'],
661
+ },
662
+ {
663
+ actualTypes: ['object'],
664
+ semanticTypes: ['array'],
665
+ },
666
+ {
667
+ actualTypes: ['boolean'],
668
+ semanticTypes: ['bool'],
669
+ },
670
+ {
671
+ actualTypes: ['text'],
672
+ semanticTypes: ['image', 'function'],
673
+ },
674
+ ];
675
+ function normalizePostgreSqlTextArray(value) {
676
+ if (!value) {
677
+ return [];
678
+ }
679
+ if (Array.isArray(value)) {
680
+ return value;
681
+ }
682
+ const trimmed = value.trim();
683
+ if (trimmed === '{}' || trimmed === '') {
684
+ return [];
685
+ }
686
+ const body = trimmed.startsWith('{') && trimmed.endsWith('}')
687
+ ? trimmed.slice(1, -1)
688
+ : trimmed;
689
+ if (!body) {
690
+ return [];
691
+ }
692
+ return body.split(',').map((item) => item.replace(/^"(.*)"$/, '$1').replace(/\\"/g, '"'));
693
+ }
694
+ function mapPostgreSqlIndexAttributes(index) {
695
+ const columnNames = normalizePostgreSqlTextArray(index.column_names);
696
+ const columnDirections = normalizePostgreSqlTextArray(index.column_directions);
697
+ return columnNames.map((name, indexPosition) => ({
698
+ name,
699
+ direction: columnDirections[indexPosition] || 'ASC',
700
+ }));
701
+ }
702
+ async function readPostgreSqlSchema(connector, translator) {
703
+ const inspection = await inspectPostgreSqlSchema(connector, translator);
704
+ return inspection.schema;
705
+ }
706
+ async function inspectPostgreSqlSchema(connector, translator) {
707
+ const result = {};
708
+ const tableStats = {};
709
+ const tableHints = (0, inspection_1.buildTargetTableHintMap)(translator.schema);
710
+ const [tables] = await connector.exec(`
711
+ SELECT
712
+ t.tablename,
713
+ c.reltuples::bigint AS estimated_rows
714
+ FROM pg_tables t
715
+ LEFT JOIN pg_class c
716
+ ON c.relname = t.tablename
717
+ WHERE t.schemaname = 'public'
718
+ ORDER BY tablename;
719
+ `);
720
+ for (const tableRow of tables) {
721
+ const tableName = tableRow.tablename;
722
+ const tableHintEntry = tableHints.get(tableName);
723
+ const tableHint = tableHintEntry?.tableDef;
724
+ const tableKey = tableHintEntry?.key || tableName;
725
+ const estimatedRows = tableRow.estimated_rows === null || tableRow.estimated_rows === undefined
726
+ ? undefined
727
+ : Number(tableRow.estimated_rows);
728
+ const [columns] = await connector.exec(`
729
+ SELECT
730
+ column_name,
731
+ data_type,
732
+ character_maximum_length,
733
+ numeric_precision,
734
+ numeric_scale,
735
+ is_nullable,
736
+ column_default,
737
+ udt_name,
738
+ is_identity
739
+ FROM information_schema.columns
740
+ WHERE table_schema = 'public'
741
+ AND table_name = '${tableName}'
742
+ ORDER BY ordinal_position;
743
+ `);
744
+ const attributes = {};
745
+ for (const column of columns) {
746
+ let attr;
747
+ const attributeHint = tableHint?.attributes?.[column.column_name];
748
+ if (column.data_type === 'USER-DEFINED') {
749
+ if (column.udt_name === 'geometry' || attributeHint?.type === 'geometry') {
750
+ attr = {
751
+ type: 'geometry',
752
+ };
753
+ }
754
+ else if (column.udt_name === 'geography' || attributeHint?.type === 'geography') {
755
+ attr = {
756
+ type: 'geography',
757
+ };
758
+ }
759
+ else {
760
+ const [enumValues] = await connector.exec(`
761
+ SELECT e.enumlabel
762
+ FROM pg_type t
763
+ JOIN pg_enum e ON e.enumtypid = t.oid
764
+ WHERE t.typname = '${column.udt_name}'
765
+ ORDER BY e.enumsortorder;
766
+ `);
767
+ attr = {
768
+ type: 'enum',
769
+ enumeration: enumValues.map((item) => item.enumlabel),
770
+ };
771
+ }
772
+ }
773
+ else {
774
+ let fullType = column.data_type;
775
+ const integerTypes = ['bigint', 'integer', 'smallint'];
776
+ if (column.character_maximum_length && !integerTypes.includes(column.data_type)) {
777
+ fullType = `${column.data_type}(${column.character_maximum_length})`;
778
+ }
779
+ else if (column.numeric_precision !== null && column.numeric_scale !== null && !integerTypes.includes(column.data_type)) {
780
+ fullType = `${column.data_type}(${column.numeric_precision},${column.numeric_scale})`;
781
+ }
782
+ else if (column.numeric_precision !== null && !integerTypes.includes(column.data_type)) {
783
+ fullType = `${column.data_type}(${column.numeric_precision})`;
784
+ }
785
+ attr = translator.reTranslateToAttribute(fullType);
786
+ }
787
+ attr = (0, inspection_1.applyInspectionAttributeHint)(attr, attributeHint, postgreSqlAttributeHintRules);
788
+ attr = (0, inspection_1.applyOakManagedAttributeFallback)(attr, column.column_name, attributeHint);
789
+ if (column.is_identity === 'YES' || column.column_name === '$$seq$$' || column.column_default?.includes('nextval')) {
790
+ attr.type = 'sequence';
791
+ attr.sequenceStart = 10000;
792
+ }
793
+ attr.notNull = attr.type === 'sequence' ? false : column.is_nullable === 'NO';
794
+ const defaultValue = parseDefaultValue(column.column_default, attr);
795
+ if (defaultValue !== undefined && attr.type !== 'sequence') {
796
+ attr.default = defaultValue;
797
+ }
798
+ attributes[column.column_name] = attr;
799
+ }
800
+ if (!(0, inspection_1.isOakManagedTable)(attributes, !!tableHint)) {
801
+ continue;
802
+ }
803
+ tableStats[tableName] = {
804
+ rowCount: Number.isFinite(estimatedRows) ? estimatedRows : undefined,
805
+ approximate: true,
806
+ };
807
+ const [foreignKeys] = await connector.exec(`
808
+ SELECT
809
+ tc.constraint_name,
810
+ kcu.column_name,
811
+ ccu.table_name AS referenced_table_name,
812
+ ccu.column_name AS referenced_column_name,
813
+ rc.delete_rule
814
+ FROM information_schema.table_constraints tc
815
+ JOIN information_schema.key_column_usage kcu
816
+ ON tc.constraint_name = kcu.constraint_name
817
+ AND tc.table_schema = kcu.table_schema
818
+ JOIN information_schema.constraint_column_usage ccu
819
+ ON tc.constraint_name = ccu.constraint_name
820
+ AND tc.table_schema = ccu.table_schema
821
+ JOIN information_schema.referential_constraints rc
822
+ ON tc.constraint_name = rc.constraint_name
823
+ AND tc.table_schema = rc.constraint_schema
824
+ WHERE tc.constraint_type = 'FOREIGN KEY'
825
+ AND tc.table_schema = 'public'
826
+ AND tc.table_name = '${tableName}';
827
+ `);
828
+ for (const foreignKey of foreignKeys) {
829
+ const attr = attributes[foreignKey.column_name];
830
+ if (!attr) {
831
+ continue;
832
+ }
833
+ attr.type = 'ref';
834
+ attr.ref = foreignKey.referenced_table_name;
835
+ switch (mapOnDeleteRule(foreignKey.delete_rule)) {
836
+ case 'cascade':
837
+ attr.onRefDelete = 'delete';
838
+ break;
839
+ case 'set null':
840
+ attr.onRefDelete = 'setNull';
841
+ break;
842
+ default:
843
+ attr.onRefDelete = 'ignore';
844
+ }
845
+ }
846
+ const [indexes] = await connector.exec(`
847
+ SELECT
848
+ i.relname AS index_name,
849
+ ix.indisunique AS is_unique,
850
+ am.amname AS index_type,
851
+ pg_get_indexdef(ix.indexrelid) AS index_def,
852
+ COALESCE(
853
+ array_agg(att.attname ORDER BY ord.ordinality)
854
+ FILTER (WHERE att.attname IS NOT NULL),
855
+ ARRAY[]::text[]
856
+ ) AS column_names,
857
+ COALESCE(
858
+ array_agg(
859
+ CASE
860
+ WHEN pg_index_column_has_property(ix.indexrelid, ord.ordinality::int, 'desc')
861
+ THEN 'DESC'
862
+ ELSE 'ASC'
863
+ END
864
+ ORDER BY ord.ordinality
865
+ ) FILTER (WHERE att.attname IS NOT NULL),
866
+ ARRAY[]::text[]
867
+ ) AS column_directions
868
+ FROM pg_class t
869
+ JOIN pg_index ix ON t.oid = ix.indrelid
870
+ JOIN pg_class i ON i.oid = ix.indexrelid
871
+ JOIN pg_am am ON am.oid = i.relam
872
+ LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS ord(attnum, ordinality)
873
+ ON TRUE
874
+ LEFT JOIN pg_attribute att
875
+ ON att.attrelid = t.oid
876
+ AND att.attnum = ord.attnum
877
+ WHERE t.relname = '${tableName}'
878
+ AND t.relkind = 'r'
879
+ AND NOT ix.indisprimary
880
+ GROUP BY i.relname, ix.indexrelid, ix.indisunique, am.amname
881
+ ORDER BY i.relname;
882
+ `);
883
+ const indexDict = {};
884
+ for (const index of indexes) {
885
+ const targetIndexHint = findMatchingPostgreSqlIndexHint(translator, tableKey, tableName, index.index_name, tableHint?.indexes);
886
+ if (index.index_type === 'gin' && index.index_def.includes('to_tsvector')) {
887
+ const tsConfigMatch = index.index_def.match(/to_tsvector\('([^']+)'/);
888
+ const tsConfig = tsConfigMatch?.[1] || 'simple';
889
+ const columns = Array.from(index.index_def.matchAll(/COALESCE\(\s*(?:"([^"]+)"|([A-Za-z0-9_$]+))/g)).map((item) => item[1] || item[2]).filter(Boolean);
890
+ let logicalName = targetIndexHint?.name || index.index_name;
891
+ if (!targetIndexHint && logicalName.startsWith(`${tableName}_`)) {
892
+ logicalName = logicalName.slice(tableName.length + 1);
893
+ }
894
+ if (!targetIndexHint && logicalName.endsWith(`_${tsConfig}`)) {
895
+ logicalName = logicalName.slice(0, -(`_${tsConfig}`.length));
896
+ }
897
+ const existed = indexDict[logicalName];
898
+ if (existed) {
899
+ const currentTsConfig = existed.config?.tsConfig;
900
+ const tsConfigs = Array.isArray(currentTsConfig)
901
+ ? currentTsConfig
902
+ : (currentTsConfig ? [currentTsConfig] : []);
903
+ if (!tsConfigs.includes(tsConfig)) {
904
+ existed.config = {
905
+ ...existed.config,
906
+ tsConfig: [...tsConfigs, tsConfig].sort(),
907
+ };
908
+ }
909
+ const originNames = existed.__originNames || [];
910
+ if (!originNames.includes(index.index_name)) {
911
+ existed.__originNames = [
912
+ ...originNames,
913
+ index.index_name,
914
+ ];
915
+ }
916
+ }
917
+ else {
918
+ indexDict[logicalName] = {
919
+ name: logicalName,
920
+ attributes: (targetIndexHint?.config?.type === 'fulltext'
921
+ ? targetIndexHint.attributes
922
+ : columns.map((name) => ({
923
+ name,
924
+ }))).map((attribute) => ({
925
+ ...attribute,
926
+ })),
927
+ config: targetIndexHint?.config?.type === 'fulltext'
928
+ ? {
929
+ ...targetIndexHint.config,
930
+ }
931
+ : {
932
+ type: 'fulltext',
933
+ tsConfig,
934
+ },
935
+ };
936
+ indexDict[logicalName].__originNames = [index.index_name];
937
+ }
938
+ continue;
939
+ }
940
+ const logicalName = targetIndexHint?.name || (index.index_name.startsWith(`${tableName}_`)
941
+ ? index.index_name.slice(tableName.length + 1)
942
+ : index.index_name);
943
+ const attributes2 = mapPostgreSqlIndexAttributes(index);
944
+ if (index.is_unique && attributes2.length === 1) {
945
+ const attr = attributes[attributes2[0].name];
946
+ if (attr) {
947
+ attr.unique = true;
948
+ continue;
949
+ }
950
+ }
951
+ const inspectedIndex = {
952
+ name: logicalName,
953
+ attributes: attributes2,
954
+ config: {
955
+ unique: index.is_unique,
956
+ type: mapPostgreSqlIndexType(index.index_type),
957
+ },
958
+ };
959
+ inspectedIndex.__originName = index.index_name;
960
+ const existed = indexDict[logicalName];
961
+ if (existed && (0, indexInspection_1.areEquivalentInspectedIndexes)(existed, inspectedIndex)) {
962
+ (0, indexInspection_1.mergeIndexOriginName)(existed, index.index_name);
963
+ continue;
964
+ }
965
+ indexDict[logicalName] = inspectedIndex;
966
+ }
967
+ const inspectedTableDef = {
968
+ ...(tableHint || {
969
+ actions: [],
970
+ actionType: 'crud',
971
+ }),
972
+ attributes,
973
+ indexes: Object.keys(indexDict).length ? Object.values(indexDict) : undefined,
974
+ };
975
+ result[tableKey] = inspectedTableDef;
976
+ }
977
+ return {
978
+ schema: result,
979
+ tableStats,
980
+ };
981
+ }
982
+ function buildPostgreSqlMigrationPlan(currentSchema, targetSchema, translator, options) {
983
+ return (0, migration_1.buildMigrationPlan)(currentSchema, targetSchema, createPgAdapter(translator), options);
984
+ }