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,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildPostgreSqlPrepareSql = buildPostgreSqlPrepareSql;
|
|
4
|
+
const textSearchConfigRegistry = {
|
|
5
|
+
chinese: {
|
|
6
|
+
getRequirement(parser = 'zhparser') {
|
|
7
|
+
return {
|
|
8
|
+
extension: parser,
|
|
9
|
+
bootstrapSql: [`DO $$
|
|
10
|
+
BEGIN
|
|
11
|
+
IF NOT EXISTS (
|
|
12
|
+
SELECT 1 FROM pg_catalog.pg_ts_config WHERE cfgname = 'chinese'
|
|
13
|
+
) THEN
|
|
14
|
+
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = ${parser});
|
|
15
|
+
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
|
|
16
|
+
END IF;
|
|
17
|
+
END $$;`],
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
function collectPostgreSqlSchemaFeatures(schema) {
|
|
23
|
+
const extensions = new Set();
|
|
24
|
+
const textSearchConfigs = new Map();
|
|
25
|
+
Object.values(schema).forEach((tableDef) => {
|
|
26
|
+
Object.values(tableDef.attributes || {}).forEach((attr) => {
|
|
27
|
+
if (attr.type === 'geometry' || attr.type === 'geography') {
|
|
28
|
+
extensions.add('postgis');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
(tableDef.indexes || []).forEach((index) => {
|
|
32
|
+
const tsConfigs = Array.isArray(index.config?.tsConfig)
|
|
33
|
+
? index.config.tsConfig
|
|
34
|
+
: [index.config?.tsConfig];
|
|
35
|
+
tsConfigs.filter(Boolean).forEach((tsConfig) => {
|
|
36
|
+
const entry = textSearchConfigRegistry[tsConfig];
|
|
37
|
+
if (!entry) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const requirement = entry.getRequirement(index.config?.chineseParser);
|
|
41
|
+
if (requirement.extension) {
|
|
42
|
+
extensions.add(requirement.extension);
|
|
43
|
+
}
|
|
44
|
+
textSearchConfigs.set(tsConfig, requirement);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
extensions,
|
|
50
|
+
textSearchConfigs,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function buildPostgreSqlPrepareSql(currentSchema, targetSchema) {
|
|
54
|
+
const sqls = [];
|
|
55
|
+
const currentFeatures = collectPostgreSqlSchemaFeatures(currentSchema);
|
|
56
|
+
const targetFeatures = collectPostgreSqlSchemaFeatures(targetSchema);
|
|
57
|
+
targetFeatures.extensions.forEach((extension) => {
|
|
58
|
+
if (!currentFeatures.extensions.has(extension)) {
|
|
59
|
+
sqls.push(`CREATE EXTENSION IF NOT EXISTS ${extension};`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
targetFeatures.textSearchConfigs.forEach((requirement, configName) => {
|
|
63
|
+
if (currentFeatures.textSearchConfigs.has(configName)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
sqls.push(...requirement.bootstrapSql);
|
|
67
|
+
});
|
|
68
|
+
return sqls;
|
|
69
|
+
}
|
|
@@ -8,12 +8,14 @@ import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
|
|
8
8
|
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
|
|
9
9
|
import { CreateEntityOption } from '../types/Translator';
|
|
10
10
|
import { DbStore, Plan } from '../types/dbStore';
|
|
11
|
+
import { MigrationPlanningOptions } from '../types/migration';
|
|
11
12
|
export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
|
12
13
|
protected countAbjointRow<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: Cxt, option: OP): number;
|
|
13
14
|
protected aggregateAbjointRowSync<T extends keyof ED, OP extends SelectOption, Cxt extends SyncContext<ED>>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): AggregationResult<ED[T]['Schema']>;
|
|
14
15
|
protected selectAbjointRow<T extends keyof ED, OP extends SelectOption>(entity: T, selection: ED[T]['Selection'], context: SyncContext<ED>, option: OP): Partial<ED[T]['Schema']>[];
|
|
15
16
|
protected updateAbjointRow<T extends keyof ED, OP extends OperateOption>(entity: T, operation: ED[T]['Operation'], context: SyncContext<ED>, option: OP): number;
|
|
16
17
|
exec(script: string, txnId?: string): Promise<void>;
|
|
18
|
+
supportsTransactionalDdl(): boolean;
|
|
17
19
|
connector: PostgreSQLConnector;
|
|
18
20
|
translator: PostgreSQLTranslator<ED>;
|
|
19
21
|
constructor(storageSchema: StorageSchema<ED>, configuration: PostgreSQLConfiguration);
|
|
@@ -22,9 +24,13 @@ export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt
|
|
|
22
24
|
aggregate<T extends keyof ED, OP extends SelectOption>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): Promise<AggregationResult<ED[T]['Schema']>>;
|
|
23
25
|
protected supportManyToOneJoin(): boolean;
|
|
24
26
|
protected supportMultipleCreate(): boolean;
|
|
27
|
+
protected supportUpdateReturning(): boolean;
|
|
25
28
|
private formResult;
|
|
29
|
+
private lockSelectedRows;
|
|
26
30
|
protected selectAbjointRowAsync<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: AsyncContext<ED>, option?: PostgreSQLSelectOption): Promise<Partial<ED[T]['Schema']>[]>;
|
|
27
31
|
protected updateAbjointRowAsync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, option?: PostgreSQLOperateOption): Promise<number>;
|
|
32
|
+
protected updateAbjointRowReturningSync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, returning: Record<string, any>, option?: PostgreSQLOperateOption): [number, Partial<ED[T]['Schema']>[]];
|
|
33
|
+
protected updateAbjointRowReturningAsync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, returning: Record<string, any>, option?: PostgreSQLOperateOption): Promise<[number, Partial<ED[T]['Schema']>[]]>;
|
|
28
34
|
operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, option: OperateOption): Promise<OperationResult<ED>>;
|
|
29
35
|
select<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, option: SelectOption): Promise<Partial<ED[T]['Schema']>[]>;
|
|
30
36
|
protected countAbjointRowAsync<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: AsyncContext<ED>, option: SelectOption): Promise<number>;
|
|
@@ -34,17 +40,25 @@ export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt
|
|
|
34
40
|
rollback(txnId: string): Promise<void>;
|
|
35
41
|
connect(): Promise<void>;
|
|
36
42
|
disconnect(): Promise<void>;
|
|
43
|
+
private shouldLogInitializeProgress;
|
|
44
|
+
private isFullManagedReset;
|
|
45
|
+
private runInitializeTransaction;
|
|
46
|
+
private ensureChineseTextSearchConfiguration;
|
|
47
|
+
private createManagedSchema;
|
|
48
|
+
private applyForeignKeysFromPlan;
|
|
49
|
+
private applyDeclaredForeignKeys;
|
|
50
|
+
private applyMissingForeignKeys;
|
|
37
51
|
initialize(option: CreateEntityOption): Promise<void>;
|
|
38
52
|
readSchema(): Promise<StorageSchema<ED>>;
|
|
39
53
|
/**
|
|
40
54
|
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
|
41
55
|
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
|
42
56
|
*/
|
|
43
|
-
makeUpgradePlan(): Promise<Plan
|
|
57
|
+
makeUpgradePlan(options?: MigrationPlanningOptions): Promise<Plan<ED>>;
|
|
44
58
|
/**
|
|
45
59
|
* 比较两个schema的不同,这里计算的是new对old的增量
|
|
46
60
|
* @param schemaOld
|
|
47
61
|
* @param schemaNew
|
|
48
62
|
*/
|
|
49
|
-
diffSchema(schemaOld: StorageSchema<
|
|
63
|
+
diffSchema(schemaOld: StorageSchema<ED>, schemaNew: StorageSchema<ED>, options?: MigrationPlanningOptions): Plan<ED>;
|
|
50
64
|
}
|
package/lib/PostgreSQL/store.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PostgreSQLStore = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const types_1 = require("oak-domain/lib/types");
|
|
5
6
|
const CascadeStore_1 = require("oak-domain/lib/store/CascadeStore");
|
|
6
7
|
const connector_1 = require("./connector");
|
|
7
8
|
const translator_1 = require("./translator");
|
|
@@ -70,6 +71,9 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
70
71
|
async exec(script, txnId) {
|
|
71
72
|
await this.connector.exec(script, txnId);
|
|
72
73
|
}
|
|
74
|
+
supportsTransactionalDdl() {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
73
77
|
connector;
|
|
74
78
|
translator;
|
|
75
79
|
constructor(storageSchema, configuration) {
|
|
@@ -94,6 +98,9 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
94
98
|
supportMultipleCreate() {
|
|
95
99
|
return true;
|
|
96
100
|
}
|
|
101
|
+
supportUpdateReturning() {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
97
104
|
formResult(entity, result) {
|
|
98
105
|
const schema = this.getSchema();
|
|
99
106
|
function resolveAttribute(entity2, r, attr, value) {
|
|
@@ -272,10 +279,34 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
272
279
|
}
|
|
273
280
|
return formSingleRow(result);
|
|
274
281
|
}
|
|
282
|
+
async lockSelectedRows(entity, rows, context, forUpdate) {
|
|
283
|
+
// PostgreSQL 不能在 outer join 的 nullable side 上直接 FOR UPDATE,
|
|
284
|
+
// 因此先完成主查询,再按根表主键补锁,保持上层 forUpdate 用法不变。
|
|
285
|
+
if (rows.length === 0) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const ids = Array.from(new Set(rows.map((row) => {
|
|
289
|
+
const id = row[types_1.PrimaryKeyAttribute];
|
|
290
|
+
(0, assert_1.default)(typeof id === 'string' && id.length > 0, `对象${String(entity)}取数据时未能获取主键,无法完成 for update 锁定`);
|
|
291
|
+
return id;
|
|
292
|
+
})));
|
|
293
|
+
const tableName = this.translator.quoteIdentifier(this.getSchema()[entity].storageName || entity);
|
|
294
|
+
const idName = this.translator.quoteIdentifier(types_1.PrimaryKeyAttribute);
|
|
295
|
+
const inClause = ids.map((id) => this.translator.escapeStringValue(id)).join(', ');
|
|
296
|
+
let sql = `SELECT ${idName} FROM ${tableName} WHERE ${idName} IN (${inClause}) FOR UPDATE`;
|
|
297
|
+
if (typeof forUpdate === 'string') {
|
|
298
|
+
sql += ` ${forUpdate}`;
|
|
299
|
+
}
|
|
300
|
+
await this.connector.exec(sql, context.getCurrentTxnId());
|
|
301
|
+
}
|
|
275
302
|
async selectAbjointRowAsync(entity, selection, context, option) {
|
|
276
303
|
const sql = this.translator.translateSelect(entity, selection, option);
|
|
277
304
|
const result = await this.connector.exec(sql, context.getCurrentTxnId());
|
|
278
|
-
|
|
305
|
+
const rows = this.formResult(entity, result[0]);
|
|
306
|
+
if (option?.forUpdate) {
|
|
307
|
+
await this.lockSelectedRows(entity, rows, context, option.forUpdate);
|
|
308
|
+
}
|
|
309
|
+
return rows;
|
|
279
310
|
}
|
|
280
311
|
async updateAbjointRowAsync(entity, operation, context, option) {
|
|
281
312
|
const { translator, connector } = this;
|
|
@@ -302,6 +333,33 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
302
333
|
}
|
|
303
334
|
}
|
|
304
335
|
}
|
|
336
|
+
updateAbjointRowReturningSync(entity, operation, context, returning, option) {
|
|
337
|
+
throw new Error('PostgreSQL store 不支持同步更新数据');
|
|
338
|
+
}
|
|
339
|
+
async updateAbjointRowReturningAsync(entity, operation, context, returning, option) {
|
|
340
|
+
const { translator, connector } = this;
|
|
341
|
+
const { action } = operation;
|
|
342
|
+
const txn = context.getCurrentTxnId();
|
|
343
|
+
switch (action) {
|
|
344
|
+
case 'create': {
|
|
345
|
+
const { data } = operation;
|
|
346
|
+
const sql = translator.translateInsert(entity, data instanceof Array ? data : [data]);
|
|
347
|
+
const result = await connector.exec(sql, txn);
|
|
348
|
+
return [result[1].rowCount || 0, []];
|
|
349
|
+
}
|
|
350
|
+
case 'remove': {
|
|
351
|
+
const sql = translator.translateRemove(entity, operation, Object.assign({}, option, { returning }));
|
|
352
|
+
const result = await connector.exec(sql, txn);
|
|
353
|
+
return [result[1].rowCount || 0, this.formResult(entity, result[0])];
|
|
354
|
+
}
|
|
355
|
+
default: {
|
|
356
|
+
(0, assert_1.default)(!['select', 'download', 'stat'].includes(action));
|
|
357
|
+
const sql = translator.translateUpdate(entity, operation, Object.assign({}, option, { returning }));
|
|
358
|
+
const result = await connector.exec(sql, txn);
|
|
359
|
+
return [result[1].rowCount || 0, this.formResult(entity, result[0])];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
305
363
|
async operate(entity, operation, context, option) {
|
|
306
364
|
const { action } = operation;
|
|
307
365
|
(0, assert_1.default)(!['select', 'download', 'stat'].includes(action), '不支持使用 select operation');
|
|
@@ -335,75 +393,159 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
335
393
|
async disconnect() {
|
|
336
394
|
await this.connector.disconnect();
|
|
337
395
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
396
|
+
shouldLogInitializeProgress(current, total, step) {
|
|
397
|
+
return total <= step || current === 1 || current === total || current % step === 0;
|
|
398
|
+
}
|
|
399
|
+
isFullManagedReset(option) {
|
|
400
|
+
return (option?.ifExists || 'drop') === 'drop';
|
|
401
|
+
}
|
|
402
|
+
async runInitializeTransaction(executor) {
|
|
403
|
+
// PostgreSQL initialize 不能再把整套 schema DDL 塞进一个大事务。
|
|
404
|
+
// 大 schema 下 DDL 锁会一直累积到 COMMIT,最终触发
|
|
405
|
+
// “out of shared memory / max_locks_per_transaction”。
|
|
406
|
+
// 这里把事务边界收敛到“单个初始化单元”(文本检索配置 / 单个实体);
|
|
407
|
+
// 既保留局部原子性,也让锁能及时释放。
|
|
408
|
+
const txn = await this.connector.startTransaction();
|
|
343
409
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
console.log('Initializing PostGIS extension for geometry support...');
|
|
363
|
-
await this.connector.exec('CREATE EXTENSION IF NOT EXISTS postgis;');
|
|
364
|
-
}
|
|
365
|
-
if (hasChineseTsConfig) {
|
|
366
|
-
console.log('Initializing Chinese text search configuration...');
|
|
367
|
-
const checkChineseConfigSql = `
|
|
368
|
-
SELECT COUNT(*) as cnt
|
|
369
|
-
FROM pg_catalog.pg_ts_config
|
|
370
|
-
WHERE cfgname = 'chinese';
|
|
410
|
+
await executor(txn);
|
|
411
|
+
await this.connector.commitTransaction(txn);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
await this.connector.rollbackTransaction(txn);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async ensureChineseTextSearchConfiguration(hasChineseTsConfig, chineseParser) {
|
|
419
|
+
if (!hasChineseTsConfig) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await this.runInitializeTransaction(async (txn) => {
|
|
423
|
+
console.log('Initializing Chinese text search configuration...');
|
|
424
|
+
const checkChineseConfigSql = `
|
|
425
|
+
SELECT COUNT(*) as cnt
|
|
426
|
+
FROM pg_catalog.pg_ts_config
|
|
427
|
+
WHERE cfgname = 'chinese';
|
|
371
428
|
`;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
|
|
429
|
+
const result = await this.connector.exec(checkChineseConfigSql, txn);
|
|
430
|
+
const count = parseInt(result[0][0]?.cnt || '0', 10);
|
|
431
|
+
if (count === 0) {
|
|
432
|
+
const createChineseConfigSql = `
|
|
433
|
+
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = ${chineseParser || 'zhparser'});
|
|
434
|
+
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
|
|
379
435
|
`;
|
|
380
|
-
|
|
381
|
-
|
|
436
|
+
await this.connector.exec(createChineseConfigSql, txn);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async createManagedSchema(option) {
|
|
441
|
+
const schema = this.getSchema();
|
|
442
|
+
const entities = Object.keys(schema);
|
|
443
|
+
console.log('Initializing PostgreSQL schema create phase...');
|
|
444
|
+
for (let idx = 0; idx < entities.length; idx++) {
|
|
445
|
+
const entity = entities[idx];
|
|
446
|
+
if (this.shouldLogInitializeProgress(idx + 1, entities.length, 10)) {
|
|
447
|
+
console.log(`Initializing PostgreSQL tables (${idx + 1}/${entities.length}): ${entity}`);
|
|
382
448
|
}
|
|
383
|
-
|
|
384
|
-
|
|
449
|
+
const sqls = this.translator.translateCreateEntity(entity, option);
|
|
450
|
+
await this.runInitializeTransaction(async (txn) => {
|
|
385
451
|
for (const sql of sqls) {
|
|
386
452
|
await this.connector.exec(sql, txn);
|
|
387
453
|
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async applyForeignKeysFromPlan(plan) {
|
|
458
|
+
const foreignKeySqls = [
|
|
459
|
+
...plan.forwardSql.filter((stmt) => /foreign key/i.test(stmt)),
|
|
460
|
+
...plan.manualSql.filter((stmt) => /add constraint .*foreign key/i.test(stmt)),
|
|
461
|
+
];
|
|
462
|
+
console.log('Initializing PostgreSQL foreign key phase...');
|
|
463
|
+
// FK 逐条独立执行,让每条 ALTER TABLE 自己提交。
|
|
464
|
+
// 这样不会再把所有引用关系的锁堆到同一个事务里。
|
|
465
|
+
for (let idx = 0; idx < foreignKeySqls.length; idx++) {
|
|
466
|
+
if (this.shouldLogInitializeProgress(idx + 1, foreignKeySqls.length, 20)) {
|
|
467
|
+
console.log(`Initializing PostgreSQL foreign keys (${idx + 1}/${foreignKeySqls.length})`);
|
|
388
468
|
}
|
|
389
|
-
await this.connector.
|
|
469
|
+
await this.connector.exec(foreignKeySqls[idx]);
|
|
390
470
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
471
|
+
}
|
|
472
|
+
async applyDeclaredForeignKeys() {
|
|
473
|
+
const plan = this.connector.diffSchema({}, this.translator.schema, this.translator, {
|
|
474
|
+
compareForeignKeys: true,
|
|
475
|
+
});
|
|
476
|
+
await this.applyForeignKeysFromPlan(plan);
|
|
477
|
+
}
|
|
478
|
+
async applyMissingForeignKeys() {
|
|
479
|
+
const inspection = await this.connector.inspectSchema(this.translator);
|
|
480
|
+
const plan = this.connector.diffSchema(inspection.schema, this.translator.schema, this.translator, {
|
|
481
|
+
compareForeignKeys: true,
|
|
482
|
+
currentTableStats: inspection.tableStats,
|
|
483
|
+
});
|
|
484
|
+
await this.applyForeignKeysFromPlan(plan);
|
|
485
|
+
}
|
|
486
|
+
async initialize(option) {
|
|
487
|
+
const schema = this.getSchema();
|
|
488
|
+
const entities = Object.keys(schema);
|
|
489
|
+
console.log('Initializing PostgreSQL prepare phase...');
|
|
490
|
+
// PostgreSQL 初始化拆成“事务外准备”和“事务内建表”两段:
|
|
491
|
+
// 扩展/解析器这类对象不能简单塞进同一个 DDL 事务,但表结构本身又希望保持原子性。
|
|
492
|
+
// ===== 第一阶段:事务外创建扩展 =====
|
|
493
|
+
let hasGeoType = false;
|
|
494
|
+
let hasChineseTsConfig = false;
|
|
495
|
+
let chineseParser = null;
|
|
496
|
+
// 扫描 schema
|
|
497
|
+
for (const entity of entities) {
|
|
498
|
+
const { attributes, indexes } = schema[entity];
|
|
499
|
+
for (const attr in attributes) {
|
|
500
|
+
if (attributes[attr].type === 'geometry') {
|
|
501
|
+
hasGeoType = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const index of indexes || []) {
|
|
505
|
+
if (index.config?.tsConfig === 'chinese' || index.config?.tsConfig?.includes('chinese')) {
|
|
506
|
+
hasChineseTsConfig = true;
|
|
507
|
+
}
|
|
508
|
+
if (index.config?.chineseParser) {
|
|
509
|
+
(0, assert_1.default)(!chineseParser || chineseParser === index.config.chineseParser, '当前定义了多个中文分词器,请保持一致');
|
|
510
|
+
chineseParser = index.config.chineseParser;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// 在事务外创建扩展
|
|
515
|
+
if (hasGeoType) {
|
|
516
|
+
console.log('Initializing PostGIS extension for geometry support...');
|
|
517
|
+
await this.connector.exec('CREATE EXTENSION IF NOT EXISTS postgis;');
|
|
518
|
+
}
|
|
519
|
+
if (hasChineseTsConfig) {
|
|
520
|
+
console.log('Initializing Chinese parser extension...');
|
|
521
|
+
await this.connector.exec(`CREATE EXTENSION IF NOT EXISTS ${chineseParser || 'zhparser'};`);
|
|
522
|
+
}
|
|
523
|
+
// ===== 第二阶段:按初始化单元分批执行事务 =====
|
|
524
|
+
// 文本检索配置、每个实体的建表 DDL、以及每条 FK 的 ALTER TABLE
|
|
525
|
+
// 都拆开提交,避免大 schema 初始化时单事务积累过多 relation locks。
|
|
526
|
+
await this.ensureChineseTextSearchConfiguration(hasChineseTsConfig, chineseParser);
|
|
527
|
+
await this.createManagedSchema(option);
|
|
528
|
+
if (this.isFullManagedReset(option)) {
|
|
529
|
+
await this.applyDeclaredForeignKeys();
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
await this.applyMissingForeignKeys();
|
|
394
533
|
}
|
|
395
534
|
}
|
|
396
535
|
// 从数据库中读取当前schema
|
|
397
536
|
readSchema() {
|
|
398
|
-
return this.
|
|
537
|
+
return this.connector.readSchema(this.translator);
|
|
399
538
|
}
|
|
400
539
|
/**
|
|
401
540
|
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
|
402
541
|
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
|
403
542
|
*/
|
|
404
|
-
async makeUpgradePlan() {
|
|
405
|
-
const
|
|
406
|
-
const plan = this.diffSchema(
|
|
543
|
+
async makeUpgradePlan(options) {
|
|
544
|
+
const inspection = await this.connector.inspectSchema(this.translator);
|
|
545
|
+
const plan = this.diffSchema(inspection.schema, this.translator.schema, {
|
|
546
|
+
...options,
|
|
547
|
+
currentTableStats: options?.currentTableStats || inspection.tableStats,
|
|
548
|
+
});
|
|
407
549
|
return plan;
|
|
408
550
|
}
|
|
409
551
|
/**
|
|
@@ -411,117 +553,8 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
411
553
|
* @param schemaOld
|
|
412
554
|
* @param schemaNew
|
|
413
555
|
*/
|
|
414
|
-
diffSchema(schemaOld, schemaNew) {
|
|
415
|
-
|
|
416
|
-
newTables: {},
|
|
417
|
-
newIndexes: {},
|
|
418
|
-
updatedIndexes: {},
|
|
419
|
-
updatedTables: {},
|
|
420
|
-
};
|
|
421
|
-
for (const table in schemaNew) {
|
|
422
|
-
// PostgreSQL 表名区分大小写(使用双引号时)
|
|
423
|
-
if (schemaOld[table]) {
|
|
424
|
-
const { attributes, indexes } = schemaOld[table];
|
|
425
|
-
const { attributes: attributesNew, indexes: indexesNew } = schemaNew[table];
|
|
426
|
-
const assignToUpdateTables = (attr, isNew) => {
|
|
427
|
-
const skipAttrs = ['$$seq$$', '$$createAt$$', '$$updateAt$$', '$$deleteAt$$', 'id'];
|
|
428
|
-
if (skipAttrs.includes(attr)) {
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (!plan.updatedTables[table]) {
|
|
432
|
-
plan.updatedTables[table] = {
|
|
433
|
-
attributes: {
|
|
434
|
-
[attr]: {
|
|
435
|
-
...attributesNew[attr],
|
|
436
|
-
isNew,
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
plan.updatedTables[table].attributes[attr] = {
|
|
443
|
-
...attributesNew[attr],
|
|
444
|
-
isNew,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
for (const attr in attributesNew) {
|
|
449
|
-
if (attributes[attr]) {
|
|
450
|
-
// 比较两次创建的属性定义是否一致
|
|
451
|
-
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
|
|
452
|
-
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
|
|
453
|
-
if (!this.translator.compareSql(sql1, sql2)) {
|
|
454
|
-
assignToUpdateTables(attr, false);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
assignToUpdateTables(attr, true);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
if (indexesNew) {
|
|
462
|
-
const assignToIndexes = (index, isNew) => {
|
|
463
|
-
if (isNew) {
|
|
464
|
-
if (plan.newIndexes[table]) {
|
|
465
|
-
plan.newIndexes[table].push(index);
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
plan.newIndexes[table] = [index];
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
if (plan.updatedIndexes[table]) {
|
|
473
|
-
plan.updatedIndexes[table].push(index);
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
plan.updatedIndexes[table] = [index];
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
|
-
const compareConfig = (config1, config2) => {
|
|
481
|
-
const unique1 = config1?.unique || false;
|
|
482
|
-
const unique2 = config2?.unique || false;
|
|
483
|
-
if (unique1 !== unique2) {
|
|
484
|
-
return false;
|
|
485
|
-
}
|
|
486
|
-
const type1 = config1?.type || 'btree';
|
|
487
|
-
const type2 = config2?.type || 'btree';
|
|
488
|
-
// tsConfig 比较
|
|
489
|
-
const tsConfig1 = config1?.tsConfig;
|
|
490
|
-
const tsConfig2 = config2?.tsConfig;
|
|
491
|
-
if (JSON.stringify(tsConfig1) !== JSON.stringify(tsConfig2)) {
|
|
492
|
-
return false;
|
|
493
|
-
}
|
|
494
|
-
return type1 === type2;
|
|
495
|
-
};
|
|
496
|
-
for (const index of indexesNew) {
|
|
497
|
-
const { name, config, attributes: indexAttrs } = index;
|
|
498
|
-
const origin = indexes?.find(ele => ele.name === name);
|
|
499
|
-
if (origin) {
|
|
500
|
-
if (JSON.stringify(indexAttrs) !== JSON.stringify(origin.attributes)) {
|
|
501
|
-
assignToIndexes(index, false);
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
if (!compareConfig(config, origin.config)) {
|
|
505
|
-
assignToIndexes(index, false);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
assignToIndexes(index, true);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
plan.newTables[table] = {
|
|
517
|
-
attributes: schemaNew[table].attributes,
|
|
518
|
-
};
|
|
519
|
-
if (schemaNew[table].indexes) {
|
|
520
|
-
plan.newIndexes[table] = schemaNew[table].indexes;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
return plan;
|
|
556
|
+
diffSchema(schemaOld, schemaNew, options) {
|
|
557
|
+
return this.connector.diffSchema(schemaOld, schemaNew, this.translator, options);
|
|
525
558
|
}
|
|
526
559
|
}
|
|
527
560
|
exports.PostgreSQLStore = PostgreSQLStore;
|
|
@@ -6,9 +6,22 @@ import { CreateEntityOption } from '../types/Translator';
|
|
|
6
6
|
export interface PostgreSQLSelectOption extends SqlSelectOption {
|
|
7
7
|
}
|
|
8
8
|
export interface PostgreSQLOperateOption extends SqlOperateOption {
|
|
9
|
+
/**
|
|
10
|
+
* PostgreSQL RETURNING 子句的投影
|
|
11
|
+
* 仅在 update/remove 时有效
|
|
12
|
+
* 返回受影响的行
|
|
13
|
+
*/
|
|
14
|
+
returning?: Record<string, any>;
|
|
9
15
|
}
|
|
10
16
|
export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict> extends SqlTranslator<ED> {
|
|
11
|
-
|
|
17
|
+
readonly maxIndexNameLength = 63;
|
|
18
|
+
private physicalIndexNameCache?;
|
|
19
|
+
private makeIndexNameCacheKey;
|
|
20
|
+
private getIndexSuffixes;
|
|
21
|
+
private ensurePhysicalIndexNameCache;
|
|
22
|
+
getEnumTypeName(entity: string, attr: string): string;
|
|
23
|
+
getPhysicalIndexName(entityName: string, tableName: string, logicalName: string, suffix?: string): string;
|
|
24
|
+
getLegacyPhysicalIndexNames(entityName: string, tableName: string, logicalName: string, suffix?: string): string[];
|
|
12
25
|
/**
|
|
13
26
|
* 将 MySQL 风格的 JSON 路径转换为 PostgreSQL 路径数组格式
|
|
14
27
|
* 例如: ".foo.bar[0].baz" -> '{foo,bar,0,baz}'
|
|
@@ -63,16 +76,22 @@ export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict
|
|
|
63
76
|
protected translateExpression<T extends keyof ED>(entity: T, alias: string, expression: RefOrExpression<keyof ED[T]["OpSchema"]>, refDict: Record<string, [string, keyof ED]>): string;
|
|
64
77
|
protected populateSelectStmt<T extends keyof ED>(projectionText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, groupByText?: string, indexFrom?: number, count?: number, option?: PostgreSQLSelectOption): string;
|
|
65
78
|
translateUpdate<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Update'], option?: OP): string;
|
|
66
|
-
translateRemove<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Remove'], option?: OP): string;
|
|
67
79
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
80
|
+
* 将 projection 转换为 RETURNING 子句的列列表
|
|
81
|
+
* @param entity 实体名
|
|
82
|
+
* @param alias 表别名
|
|
83
|
+
* @param projection 投影定义
|
|
70
84
|
*/
|
|
71
|
-
private
|
|
85
|
+
private buildReturningClause;
|
|
72
86
|
/**
|
|
73
|
-
*
|
|
87
|
+
* 验证操作参数的合法性
|
|
74
88
|
*/
|
|
75
|
-
private
|
|
89
|
+
private validateOperationParams;
|
|
90
|
+
/**
|
|
91
|
+
* 添加RETURNING子句
|
|
92
|
+
*/
|
|
93
|
+
private appendReturningClause;
|
|
94
|
+
translateRemove<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Remove'], option?: OP): string;
|
|
76
95
|
/**
|
|
77
96
|
* 生成 PostgreSQL UPSERT 语句
|
|
78
97
|
* INSERT ... ON CONFLICT (key) DO UPDATE SET ...
|
|
@@ -84,7 +103,7 @@ export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict
|
|
|
84
103
|
* 将 PostgreSQL 返回的 Type 回译成 oak 的类型,是 populateDataTypeDef 的反函数
|
|
85
104
|
* @param type PostgreSQL 类型字符串
|
|
86
105
|
*/
|
|
87
|
-
|
|
106
|
+
reTranslateToAttribute(type: string): Attribute;
|
|
88
107
|
/**
|
|
89
108
|
* 从 PostgreSQL 数据库读取当前的 schema 结构
|
|
90
109
|
*/
|
|
@@ -95,6 +114,7 @@ export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict
|
|
|
95
114
|
* @param attrDef 属性定义
|
|
96
115
|
*/
|
|
97
116
|
translateAttributeDef(attr: string, attrDef: Attribute): string;
|
|
117
|
+
translateColumnDefinition(entity: string, attr: string, attrDef: Attribute): string;
|
|
98
118
|
/**
|
|
99
119
|
* 比较两个 SQL 语句是否等价(用于 schema diff)
|
|
100
120
|
* 忽略空格、大小写等格式差异
|