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,2 @@
1
+ import { MigrationSchema } from '../types/migration';
2
+ export declare function buildPostgreSqlPrepareSql(currentSchema: MigrationSchema, targetSchema: MigrationSchema): string[];
@@ -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<any>, schemaNew: StorageSchema<any>): Plan;
63
+ diffSchema(schemaOld: StorageSchema<ED>, schemaNew: StorageSchema<ED>, options?: MigrationPlanningOptions): Plan<ED>;
50
64
  }
@@ -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
- return this.formResult(entity, result[0]);
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
- async initialize(option) {
339
- // PG的DDL支持事务,所以这里直接用一个事务包裹所有的初始化操作
340
- const txn = await this.connector.startTransaction({
341
- isolationLevel: 'serializable',
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
- const schema = this.getSchema();
345
- let hasGeoType = false;
346
- let hasChineseTsConfig = false;
347
- for (const entity in schema) {
348
- const { attributes, indexes } = schema[entity];
349
- for (const attr in attributes) {
350
- const { type } = attributes[attr];
351
- if (type === 'geometry') {
352
- hasGeoType = true;
353
- }
354
- }
355
- for (const index of indexes || []) {
356
- if (index.config?.tsConfig === 'chinese' || index.config?.tsConfig?.includes('chinese')) {
357
- hasChineseTsConfig = true;
358
- }
359
- }
360
- }
361
- if (hasGeoType) {
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
- const result = await this.connector.exec(checkChineseConfigSql);
373
- const count = parseInt(result[0][0]?.cnt || '0', 10);
374
- if (count === 0) {
375
- const createChineseConfigSql = `
376
- CREATE EXTENSION IF NOT EXISTS zhparser;
377
- CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
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
- await this.connector.exec(createChineseConfigSql);
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
- for (const entity in schema) {
384
- const sqls = this.translator.translateCreateEntity(entity, option);
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.commitTransaction(txn);
469
+ await this.connector.exec(foreignKeySqls[idx]);
390
470
  }
391
- catch (error) {
392
- await this.connector.rollbackTransaction(txn);
393
- throw error;
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.translator.readSchema((sql) => this.connector.exec(sql));
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 originSchema = await this.readSchema();
406
- const plan = this.diffSchema(originSchema, this.translator.schema);
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
- const plan = {
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
- private getEnumTypeName;
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
- * PostgreSQL专用的结构化JOIN分析
69
- * 返回结构化的JOIN信息,而不是拼接好的FROM字符串
80
+ * 将 projection 转换为 RETURNING 子句的列列表
81
+ * @param entity 实体名
82
+ * @param alias 表别名
83
+ * @param projection 投影定义
70
84
  */
71
- private analyzeJoinStructured;
85
+ private buildReturningClause;
72
86
  /**
73
- * 构建JOIN条件(用于UPDATE/DELETE的WHERE子句)
87
+ * 验证操作参数的合法性
74
88
  */
75
- private buildJoinConditions;
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
- private reTranslateToAttribute;
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
  * 忽略空格、大小写等格式差异