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.
- package/README.md +5 -1
- package/lib/MySQL/connector.d.ts +9 -1
- package/lib/MySQL/connector.js +33 -14
- package/lib/MySQL/migration.d.ts +10 -0
- package/lib/MySQL/migration.js +649 -0
- package/lib/MySQL/store.d.ts +19 -2
- package/lib/MySQL/store.js +159 -110
- package/lib/MySQL/translator.d.ts +5 -1
- package/lib/MySQL/translator.js +47 -14
- package/lib/PostgreSQL/connector.d.ts +10 -0
- package/lib/PostgreSQL/connector.js +58 -51
- package/lib/PostgreSQL/migration.d.ts +10 -0
- package/lib/PostgreSQL/migration.js +984 -0
- package/lib/PostgreSQL/prepare.d.ts +2 -0
- package/lib/PostgreSQL/prepare.js +69 -0
- package/lib/PostgreSQL/store.d.ts +16 -2
- package/lib/PostgreSQL/store.js +196 -163
- package/lib/PostgreSQL/translator.d.ts +28 -8
- package/lib/PostgreSQL/translator.js +208 -226
- package/lib/index.d.ts +1 -0
- package/lib/migration.d.ts +27 -0
- package/lib/migration.js +1029 -0
- package/lib/sqlTranslator.d.ts +5 -1
- package/lib/sqlTranslator.js +12 -4
- package/lib/types/dbStore.d.ts +8 -15
- package/lib/types/migration.d.ts +251 -0
- package/lib/types/migration.js +2 -0
- package/lib/utils/indexInspection.d.ts +4 -0
- package/lib/utils/indexInspection.js +32 -0
- package/lib/utils/indexName.d.ts +15 -0
- package/lib/utils/indexName.js +76 -0
- package/lib/utils/inspection.d.ts +13 -0
- package/lib/utils/inspection.js +56 -0
- package/package.json +5 -2
|
@@ -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
|
+
}
|