oak-db 3.3.13 → 3.3.14

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.
@@ -20,27 +20,22 @@ class MySqlConnector {
20
20
  return this.pool.end();
21
21
  }
22
22
  async startTransaction(option) {
23
- // 防止用户配置connection可复用
24
- const startInner = async () => {
25
- const connection = await this.pool.getConnection();
26
- // 分配出来的connection不能被别的事务占据
27
- for (const txn2 in this.txnDict) {
28
- if (this.txnDict[txn2] === connection) {
29
- return new Promise((resolve) => {
30
- this.pool.on('release', () => resolve(startInner()));
31
- });
32
- }
33
- }
23
+ const connection = await this.pool.getConnection();
24
+ const id = (0, uuid_1.v4)();
25
+ try {
34
26
  await connection.beginTransaction();
35
- const id = (0, uuid_1.v4)();
36
27
  // console.log('start_txn', id, connection.threadId);
37
28
  this.txnDict[id] = connection;
38
29
  if (option?.isolationLevel) {
39
30
  await connection.query(`SET TRANSACTION ISOLATION LEVEL ${option.isolationLevel};`);
40
31
  }
41
32
  return id;
42
- };
43
- return startInner();
33
+ }
34
+ catch (err) { // 出错时释放连接
35
+ connection.release();
36
+ delete this.txnDict[id];
37
+ throw err;
38
+ }
44
39
  }
45
40
  async exec(sql, txn) {
46
41
  if (process.env.NODE_ENV === 'development') {
@@ -22,9 +22,12 @@ export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt exte
22
22
  aggregate<T extends keyof ED, OP extends SelectOption>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): Promise<AggregationResult<ED[T]['Schema']>>;
23
23
  protected supportManyToOneJoin(): boolean;
24
24
  protected supportMultipleCreate(): boolean;
25
+ protected supportUpdateReturning(): boolean;
25
26
  private formResult;
26
27
  protected selectAbjointRowAsync<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: AsyncContext<ED>, option?: MySqlSelectOption): Promise<Partial<ED[T]['Schema']>[]>;
28
+ protected updateAbjointRowReturningSync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, returning: Record<string, any>, option?: MysqlOperateOption): [number, Partial<ED[T]['Schema']>[]];
27
29
  protected updateAbjointRowAsync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, option?: MysqlOperateOption): Promise<number>;
30
+ protected updateAbjointRowReturningAsync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, returning?: Record<string, any>, option?: MysqlOperateOption): Promise<[number, Partial<ED[T]['Schema']>[]]>;
28
31
  operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, option: OperateOption): Promise<OperationResult<ED>>;
29
32
  select<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, option: SelectOption): Promise<Partial<ED[T]['Schema']>[]>;
30
33
  protected countAbjointRowAsync<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: AsyncContext<ED>, option: SelectOption): Promise<number>;
@@ -61,6 +61,9 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
61
61
  supportMultipleCreate() {
62
62
  return true;
63
63
  }
64
+ supportUpdateReturning() {
65
+ return false;
66
+ }
64
67
  formResult(entity, result) {
65
68
  const schema = this.getSchema();
66
69
  /* function resolveObject(r: Record<string, any>, path: string, value: any) {
@@ -236,6 +239,9 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
236
239
  const result = await this.connector.exec(sql, context.getCurrentTxnId());
237
240
  return this.formResult(entity, result[0]);
238
241
  }
242
+ updateAbjointRowReturningSync(entity, operation, context, returning, option) {
243
+ throw new Error('Mysql store 不支持同步更新数据');
244
+ }
239
245
  async updateAbjointRowAsync(entity, operation, context, option) {
240
246
  const { translator, connector } = this;
241
247
  const { action } = operation;
@@ -251,7 +257,7 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
251
257
  const sql = translator.translateRemove(entity, operation, option);
252
258
  const result = await connector.exec(sql, txn);
253
259
  // todo 这里对sorter和indexfrom/count的支持不完整
254
- return result[0].changedRows;
260
+ return result[0].affectedRows;
255
261
  }
256
262
  default: {
257
263
  (0, assert_1.default)(!['select', 'download', 'stat'].includes(action));
@@ -262,6 +268,9 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
262
268
  }
263
269
  }
264
270
  }
271
+ async updateAbjointRowReturningAsync(entity, operation, context, returning, option) {
272
+ throw new Error('MySQL store不支持返回更新数据');
273
+ }
265
274
  async operate(entity, operation, context, option) {
266
275
  const { action } = operation;
267
276
  (0, assert_1.default)(!['select', 'download', 'stat'].includes(action), '现在不支持使用select operation');
@@ -1069,9 +1069,12 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
1069
1069
  if (sorterText) {
1070
1070
  sql += ` order by ${sorterText}`;
1071
1071
  }
1072
- if (typeof indexFrom === 'number') {
1073
- (0, assert_1.default)(typeof count === 'number');
1074
- sql += ` limit ${indexFrom}, ${count}`;
1072
+ if (typeof count === 'number') {
1073
+ // MySQL UPDATE 不支持 LIMIT offset, count 语法
1074
+ if (typeof indexFrom === 'number' && indexFrom > 0) {
1075
+ throw new Error('MySQL does not support LIMIT with OFFSET in UPDATE statements. Use indexFrom=0 or omit indexFrom.');
1076
+ }
1077
+ sql += ` limit ${count}`;
1075
1078
  }
1076
1079
  return sql;
1077
1080
  }
@@ -1087,9 +1090,12 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
1087
1090
  if (sorterText) {
1088
1091
  sql += ` order by ${sorterText}`;
1089
1092
  }
1090
- if (typeof indexFrom === 'number') {
1091
- (0, assert_1.default)(typeof count === 'number');
1092
- sql += ` limit ${indexFrom}, ${count}`;
1093
+ if (typeof count === 'number') {
1094
+ // MySQL DELETE 不支持 LIMIT offset, count 语法
1095
+ if (typeof indexFrom === 'number' && indexFrom > 0) {
1096
+ throw new Error('MySQL does not support LIMIT with OFFSET in DELETE statements. Use indexFrom=0 or omit indexFrom.');
1097
+ }
1098
+ sql += ` limit ${count}`;
1093
1099
  }
1094
1100
  return sql;
1095
1101
  }
@@ -1105,9 +1111,12 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
1105
1111
  if (sorterText) {
1106
1112
  sql += ` order by ${sorterText}`;
1107
1113
  }
1108
- if (typeof indexFrom === 'number') {
1109
- (0, assert_1.default)(typeof count === 'number');
1110
- sql += ` limit ${indexFrom}, ${count}`;
1114
+ if (typeof count === 'number') {
1115
+ // MySQL UPDATE 不支持 LIMIT offset, count 语法(软删除使用UPDATE)
1116
+ if (typeof indexFrom === 'number' && indexFrom > 0) {
1117
+ throw new Error('MySQL does not support LIMIT with OFFSET in UPDATE statements (soft delete). Use indexFrom=0 or omit indexFrom.');
1118
+ }
1119
+ sql += ` limit ${count}`;
1111
1120
  }
1112
1121
  return sql;
1113
1122
  }
@@ -36,37 +36,27 @@ class PostgreSQLConnector {
36
36
  await this.pool.end();
37
37
  }
38
38
  async startTransaction(option) {
39
- const startInner = async () => {
40
- const connection = await this.pool.connect();
41
- // 添加:检测连接是否已被其他事务占用
42
- for (const txn2 in this.txnDict) {
43
- if (this.txnDict[txn2] === connection) {
44
- return new Promise((resolve) => {
45
- this.pool.on('release', () => resolve(startInner()));
46
- });
47
- }
48
- }
49
- try {
50
- let beginStmt = 'BEGIN';
51
- if (option?.isolationLevel) {
52
- // PostgreSQL 隔离级别:
53
- // READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
54
- // 注意: PostgreSQL 的 READ UNCOMMITTED 行为等同于 READ COMMITTED
55
- const level = this.mapIsolationLevel(option.isolationLevel);
56
- beginStmt = `BEGIN ISOLATION LEVEL ${level}`;
57
- }
58
- await connection.query(beginStmt);
59
- const id = (0, uuid_1.v4)();
60
- this.txnDict[id] = connection;
61
- return id;
62
- }
63
- catch (error) {
64
- // 如果启动事务失败,释放连接
65
- connection.release();
66
- throw error;
39
+ const connection = await this.pool.connect();
40
+ const id = (0, uuid_1.v4)();
41
+ try {
42
+ let beginStmt = 'BEGIN';
43
+ if (option?.isolationLevel) {
44
+ // PostgreSQL 隔离级别:
45
+ // READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
46
+ // 注意: PostgreSQL 的 READ UNCOMMITTED 行为等同于 READ COMMITTED
47
+ const level = this.mapIsolationLevel(option.isolationLevel);
48
+ beginStmt = `BEGIN ISOLATION LEVEL ${level}`;
67
49
  }
68
- };
69
- return startInner();
50
+ await connection.query(beginStmt);
51
+ this.txnDict[id] = connection;
52
+ return id;
53
+ }
54
+ catch (error) {
55
+ // 如果启动事务失败,释放连接
56
+ connection.release();
57
+ delete this.txnDict[id];
58
+ throw error;
59
+ }
70
60
  }
71
61
  /**
72
62
  * 映射隔离级别到 PostgreSQL 语法
@@ -90,26 +80,17 @@ class PostgreSQLConnector {
90
80
  if (process.env.NODE_ENV === 'development') {
91
81
  // console.log(`SQL: ${sql}; \n`);
92
82
  }
93
- try {
94
- let result;
95
- if (txn) {
96
- const connection = this.txnDict[txn];
97
- (0, assert_1.default)(connection, `Transaction ${txn} not found`);
98
- result = await connection.query(sql);
99
- }
100
- else {
101
- result = await this.pool.query(sql);
102
- }
103
- // 返回格式与 mysql2 兼容: [rows, fields/result]
104
- return [result.rows, result];
83
+ let result;
84
+ if (txn) {
85
+ const connection = this.txnDict[txn];
86
+ (0, assert_1.default)(connection, `Transaction ${txn} not found`);
87
+ result = await connection.query(sql);
105
88
  }
106
- catch (error) {
107
- // 增强错误信息
108
- const enhancedError = new Error(`PostgreSQL query failed: ${error.message}\nSQL: ${sql.slice(0, 500)}${sql.length > 500 ? '...' : ''}`);
109
- enhancedError.originalError = error;
110
- enhancedError.sql = sql;
111
- throw enhancedError;
89
+ else {
90
+ result = await this.pool.query(sql);
112
91
  }
92
+ // 返回格式与 mysql2 兼容: [rows, fields/result]
93
+ return [result.rows, result];
113
94
  }
114
95
  async commitTransaction(txn) {
115
96
  const connection = this.txnDict[txn];
@@ -22,9 +22,12 @@ export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt
22
22
  aggregate<T extends keyof ED, OP extends SelectOption>(entity: T, aggregation: ED[T]['Aggregation'], context: Cxt, option: OP): Promise<AggregationResult<ED[T]['Schema']>>;
23
23
  protected supportManyToOneJoin(): boolean;
24
24
  protected supportMultipleCreate(): boolean;
25
+ protected supportUpdateReturning(): boolean;
25
26
  private formResult;
26
27
  protected selectAbjointRowAsync<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: AsyncContext<ED>, option?: PostgreSQLSelectOption): Promise<Partial<ED[T]['Schema']>[]>;
27
28
  protected updateAbjointRowAsync<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: AsyncContext<ED>, option?: PostgreSQLOperateOption): Promise<number>;
29
+ 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']>[]];
30
+ 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
31
  operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, option: OperateOption): Promise<OperationResult<ED>>;
29
32
  select<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, option: SelectOption): Promise<Partial<ED[T]['Schema']>[]>;
30
33
  protected countAbjointRowAsync<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, context: AsyncContext<ED>, option: SelectOption): Promise<number>;
@@ -94,6 +94,9 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
94
94
  supportMultipleCreate() {
95
95
  return true;
96
96
  }
97
+ supportUpdateReturning() {
98
+ return true;
99
+ }
97
100
  formResult(entity, result) {
98
101
  const schema = this.getSchema();
99
102
  function resolveAttribute(entity2, r, attr, value) {
@@ -302,6 +305,33 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
302
305
  }
303
306
  }
304
307
  }
308
+ updateAbjointRowReturningSync(entity, operation, context, returning, option) {
309
+ throw new Error('PostgreSQL store 不支持同步更新数据');
310
+ }
311
+ async updateAbjointRowReturningAsync(entity, operation, context, returning, option) {
312
+ const { translator, connector } = this;
313
+ const { action } = operation;
314
+ const txn = context.getCurrentTxnId();
315
+ switch (action) {
316
+ case 'create': {
317
+ const { data } = operation;
318
+ const sql = translator.translateInsert(entity, data instanceof Array ? data : [data]);
319
+ const result = await connector.exec(sql, txn);
320
+ return [result[1].rowCount || 0, []];
321
+ }
322
+ case 'remove': {
323
+ const sql = translator.translateRemove(entity, operation, Object.assign({}, option, { returning }));
324
+ const result = await connector.exec(sql, txn);
325
+ return [result[1].rowCount || 0, this.formResult(entity, result[0])];
326
+ }
327
+ default: {
328
+ (0, assert_1.default)(!['select', 'download', 'stat'].includes(action));
329
+ const sql = translator.translateUpdate(entity, operation, Object.assign({}, option, { returning }));
330
+ const result = await connector.exec(sql, txn);
331
+ return [result[1].rowCount || 0, this.formResult(entity, result[0])];
332
+ }
333
+ }
334
+ }
305
335
  async operate(entity, operation, context, option) {
306
336
  const { action } = operation;
307
337
  (0, assert_1.default)(!['select', 'download', 'stat'].includes(action), '不支持使用 select operation');
@@ -336,50 +366,62 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
336
366
  await this.connector.disconnect();
337
367
  }
338
368
  async initialize(option) {
339
- // PG的DDL支持事务,所以这里直接用一个事务包裹所有的初始化操作
369
+ const schema = this.getSchema();
370
+ // ===== 第一阶段:事务外创建扩展 =====
371
+ let hasGeoType = false;
372
+ let hasChineseTsConfig = false;
373
+ let chineseParser = null;
374
+ // 扫描 schema
375
+ for (const entity in schema) {
376
+ const { attributes, indexes } = schema[entity];
377
+ for (const attr in attributes) {
378
+ if (attributes[attr].type === 'geometry') {
379
+ hasGeoType = true;
380
+ }
381
+ }
382
+ for (const index of indexes || []) {
383
+ if (index.config?.tsConfig === 'chinese' || index.config?.tsConfig?.includes('chinese')) {
384
+ hasChineseTsConfig = true;
385
+ }
386
+ if (index.config?.chineseParser) {
387
+ (0, assert_1.default)(!chineseParser || chineseParser === index.config.chineseParser, '当前定义了多个中文分词器,请保持一致');
388
+ chineseParser = index.config.chineseParser;
389
+ }
390
+ }
391
+ }
392
+ // 在事务外创建扩展
393
+ if (hasGeoType) {
394
+ console.log('Initializing PostGIS extension for geometry support...');
395
+ await this.connector.exec('CREATE EXTENSION IF NOT EXISTS postgis;');
396
+ }
397
+ if (hasChineseTsConfig) {
398
+ console.log('Initializing Chinese parser extension...');
399
+ await this.connector.exec(`CREATE EXTENSION IF NOT EXISTS ${chineseParser || 'zhparser'};`);
400
+ }
401
+ // ===== 第二阶段:事务内创建配置和表 =====
340
402
  const txn = await this.connector.startTransaction({
341
403
  isolationLevel: 'serializable',
342
404
  });
343
405
  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
- }
406
+ // 创建中文文本搜索配置
365
407
  if (hasChineseTsConfig) {
366
408
  console.log('Initializing Chinese text search configuration...');
367
409
  const checkChineseConfigSql = `
368
- SELECT COUNT(*) as cnt
369
- FROM pg_catalog.pg_ts_config
370
- WHERE cfgname = 'chinese';
410
+ SELECT COUNT(*) as cnt
411
+ FROM pg_catalog.pg_ts_config
412
+ WHERE cfgname = 'chinese';
371
413
  `;
372
- const result = await this.connector.exec(checkChineseConfigSql);
414
+ const result = await this.connector.exec(checkChineseConfigSql, txn);
373
415
  const count = parseInt(result[0][0]?.cnt || '0', 10);
374
416
  if (count === 0) {
375
417
  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;
418
+ CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = ${chineseParser || 'zhparser'});
419
+ ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
379
420
  `;
380
- await this.connector.exec(createChineseConfigSql);
421
+ await this.connector.exec(createChineseConfigSql, txn);
381
422
  }
382
423
  }
424
+ // 创建实体表
383
425
  for (const entity in schema) {
384
426
  const sqls = this.translator.translateCreateEntity(entity, option);
385
427
  for (const sql of sqls) {
@@ -6,6 +6,12 @@ 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
  private getEnumTypeName;
@@ -63,16 +69,22 @@ export declare class PostgreSQLTranslator<ED extends EntityDict & BaseEntityDict
63
69
  protected translateExpression<T extends keyof ED>(entity: T, alias: string, expression: RefOrExpression<keyof ED[T]["OpSchema"]>, refDict: Record<string, [string, keyof ED]>): string;
64
70
  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
71
  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
72
  /**
68
- * PostgreSQL专用的结构化JOIN分析
69
- * 返回结构化的JOIN信息,而不是拼接好的FROM字符串
73
+ * 将 projection 转换为 RETURNING 子句的列列表
74
+ * @param entity 实体名
75
+ * @param alias 表别名
76
+ * @param projection 投影定义
70
77
  */
71
- private analyzeJoinStructured;
78
+ private buildReturningClause;
72
79
  /**
73
- * 构建JOIN条件(用于UPDATE/DELETE的WHERE子句)
80
+ * 验证操作参数的合法性
74
81
  */
75
- private buildJoinConditions;
82
+ private validateOperationParams;
83
+ /**
84
+ * 添加RETURNING子句
85
+ */
86
+ private appendReturningClause;
87
+ translateRemove<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Remove'], option?: OP): string;
76
88
  /**
77
89
  * 生成 PostgreSQL UPSERT 语句
78
90
  * INSERT ... ON CONFLICT (key) DO UPDATE SET ...
@@ -7,7 +7,6 @@ const util_1 = require("util");
7
7
  const lodash_1 = require("lodash");
8
8
  const types_1 = require("oak-domain/lib/types");
9
9
  const sqlTranslator_1 = require("../sqlTranslator");
10
- const relation_1 = require("oak-domain/lib/store/relation");
11
10
  const GeoTypes = [
12
11
  {
13
12
  type: 'point',
@@ -1483,10 +1482,11 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
1483
1482
  translateUpdate(entity, operation, option) {
1484
1483
  const { attributes } = this.schema[entity];
1485
1484
  const { filter, sorter, indexFrom, count, data } = operation;
1486
- (0, assert_1.default)(!sorter, '当前update不支持sorter行为');
1487
- // 使用结构化的JOIN分析
1488
- const { aliasDict, filterRefAlias, mainTable, mainAlias, joinInfos, currentNumber } = this.analyzeJoinStructured(entity, { filter, sorter });
1489
- // 构建SET子句 - PostgreSQL中SET子句不能使用表别名前缀
1485
+ // 参数验证
1486
+ this.validateOperationParams(sorter, indexFrom, count);
1487
+ const mainTable = this.getStorageName(entity);
1488
+ const mainAlias = `${entity}_1`;
1489
+ // 构建 SET 子句
1490
1490
  const setClauses = [];
1491
1491
  for (const attr in data) {
1492
1492
  (0, assert_1.default)(attributes.hasOwnProperty(attr));
@@ -1494,73 +1494,93 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
1494
1494
  setClauses.push(`"${attr}" = ${value}`);
1495
1495
  }
1496
1496
  const setClause = setClauses.join(', ');
1497
- // 构建过滤条件
1498
- const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, option);
1499
- let sql;
1500
- if (joinInfos.length === 0) {
1501
- // 单表更新
1502
- sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
1503
- if (filterText) {
1504
- sql += ` WHERE ${filterText}`;
1505
- }
1497
+ // 统一使用子查询方案
1498
+ const subqueryOperation = {
1499
+ data: { [types_1.PrimaryKeyAttribute]: 1 },
1500
+ filter: filter,
1501
+ indexFrom: indexFrom,
1502
+ count: count
1503
+ };
1504
+ const subquery = this.translateSelect(entity, subqueryOperation, { includedDeleted: option?.includedDeleted });
1505
+ let sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause} WHERE "${types_1.PrimaryKeyAttribute}" IN (${subquery})`;
1506
+ // 添加 RETURNING 子句
1507
+ return this.appendReturningClause(sql, entity, mainAlias, option);
1508
+ }
1509
+ /**
1510
+ * 将 projection 转换为 RETURNING 子句的列列表
1511
+ * @param entity 实体名
1512
+ * @param alias 表别名
1513
+ * @param projection 投影定义
1514
+ */
1515
+ buildReturningClause(entity, alias, projection) {
1516
+ const columns = [];
1517
+ const { attributes } = this.schema[entity];
1518
+ for (const attr in projection) {
1519
+ // if (!attributes[attr]) {
1520
+ // // 如果存在entity这个attr
1521
+ // const entityDesc = attributes['entity']
1522
+ // const entityIdDesc = attributes['entityId']
1523
+ // if (entityDesc && entityIdDesc && ((entityDesc.ref as string[])?.includes(attr))) {
1524
+ // // 特殊处理 entity 和 entityId 属性
1525
+ // }
1526
+ // continue;
1527
+ // }
1528
+ // 只能返回更新列的相关字段,所以直接assert
1529
+ (0, assert_1.default)(attributes.hasOwnProperty(attr), `RETURNING语法只能返回更新列的字段,但在实体 ${String(entity)} 中未找到原生属性「${attr}」的定义`);
1530
+ const dataType = attributes[attr].type;
1531
+ // 处理特殊类型的投影
1532
+ const columnExpr = this.translateAttrProjection(dataType, alias, attr);
1533
+ // RETURNING 需要明确的别名(不带表前缀)
1534
+ columns.push(`${columnExpr} AS "${attr}"`);
1535
+ }
1536
+ if (columns.length === 0) {
1537
+ throw new Error(`No valid columns in RETURNING clause for entity ${String(entity)}`);
1538
+ }
1539
+ return columns.join(', ');
1540
+ }
1541
+ /**
1542
+ * 验证操作参数的合法性
1543
+ */
1544
+ validateOperationParams(sorter, indexFrom, count) {
1545
+ (0, assert_1.default)(!sorter, '当前update/remove不支持sorter行为');
1546
+ // 对于limit,如果有indexFrom则一定有count
1547
+ if (typeof indexFrom === 'number') {
1548
+ (0, assert_1.default)(typeof count === 'number' && count > 0);
1506
1549
  }
1507
- else {
1508
- // 多表更新 - PostgreSQL语法: UPDATE main SET ... FROM other_tables WHERE join_conditions AND filter
1509
- sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
1510
- // FROM子句包含所有JOIN的表
1511
- const fromTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
1512
- sql += ` FROM ${fromTables}`;
1513
- // WHERE子句包含JOIN条件和过滤条件
1514
- const joinConditions = this.buildJoinConditions(joinInfos);
1515
- let whereClause = joinConditions;
1516
- if (filterText) {
1517
- whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
1518
- }
1519
- if (whereClause) {
1520
- sql += ` WHERE ${whereClause}`;
1521
- }
1550
+ }
1551
+ /**
1552
+ * 添加RETURNING子句
1553
+ */
1554
+ appendReturningClause(sql, entity, mainAlias, option) {
1555
+ if (option?.returning) {
1556
+ const returningClause = this.buildReturningClause(entity, mainAlias, option.returning);
1557
+ return `${sql} RETURNING ${returningClause}`;
1522
1558
  }
1523
1559
  return sql;
1524
1560
  }
1525
1561
  translateRemove(entity, operation, option) {
1526
1562
  const { data, filter, sorter, indexFrom, count } = operation;
1527
- (0, assert_1.default)(!sorter, '当前remove不支持sorter行为');
1528
- // 使用结构化的JOIN分析
1529
- const { aliasDict, filterRefAlias, mainTable, mainAlias, joinInfos, currentNumber } = this.analyzeJoinStructured(entity, { filter, sorter });
1530
- // 构建过滤条件
1531
- const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, { includedDeleted: option?.includedDeleted });
1532
1563
  const { attributes } = this.schema[entity];
1564
+ // 参数验证
1565
+ this.validateOperationParams(sorter, indexFrom, count);
1566
+ const mainTable = this.getStorageName(entity);
1567
+ const mainAlias = `${entity}_1`;
1568
+ // 构建子查询
1569
+ const subqueryOperation = {
1570
+ data: { [types_1.PrimaryKeyAttribute]: 1 },
1571
+ filter: filter,
1572
+ indexFrom: indexFrom,
1573
+ count: count
1574
+ };
1575
+ const subquery = this.translateSelect(entity, subqueryOperation, { includedDeleted: option?.includedDeleted });
1576
+ let sql;
1533
1577
  if (option?.deletePhysically) {
1534
1578
  // 物理删除
1535
1579
  (0, assert_1.default)((0, lodash_1.difference)(Object.keys(data), [types_1.UpdateAtAttribute, types_1.DeleteAtAttribute]).length === 0);
1536
- let sql;
1537
- if (joinInfos.length === 0) {
1538
- // 单表删除
1539
- sql = `DELETE FROM "${mainTable}" AS "${mainAlias}"`;
1540
- if (filterText) {
1541
- sql += ` WHERE ${filterText}`;
1542
- }
1543
- }
1544
- else {
1545
- // 多表删除 - PostgreSQL语法: DELETE FROM main USING other_tables WHERE join_conditions AND filter
1546
- sql = `DELETE FROM "${mainTable}" AS "${mainAlias}"`;
1547
- // USING子句包含所有JOIN的表
1548
- const usingTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
1549
- sql += ` USING ${usingTables}`;
1550
- // WHERE子句包含JOIN条件和过滤条件
1551
- const joinConditions = this.buildJoinConditions(joinInfos);
1552
- let whereClause = joinConditions;
1553
- if (filterText) {
1554
- whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
1555
- }
1556
- if (whereClause) {
1557
- sql += ` WHERE ${whereClause}`;
1558
- }
1559
- }
1560
- return sql;
1580
+ sql = `DELETE FROM "${mainTable}" AS "${mainAlias}" WHERE "${types_1.PrimaryKeyAttribute}" IN (${subquery})`;
1561
1581
  }
1562
1582
  else {
1563
- // 软删除 - 实际是UPDATE操作
1583
+ // 软删除- 实际是 UPDATE
1564
1584
  const setClauses = [];
1565
1585
  for (const attr in data) {
1566
1586
  (0, assert_1.default)([types_1.TriggerDataAttribute, types_1.TriggerUuidAttribute, types_1.DeleteAtAttribute, types_1.UpdateAtAttribute].includes(attr));
@@ -1568,164 +1588,10 @@ class PostgreSQLTranslator extends sqlTranslator_1.SqlTranslator {
1568
1588
  setClauses.push(`"${attr}" = ${value}`);
1569
1589
  }
1570
1590
  const setClause = setClauses.join(', ');
1571
- let sql;
1572
- if (joinInfos.length === 0) {
1573
- // 单表更新
1574
- sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
1575
- if (filterText) {
1576
- sql += ` WHERE ${filterText}`;
1577
- }
1578
- }
1579
- else {
1580
- // 多表更新
1581
- sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause}`;
1582
- const fromTables = joinInfos.map(j => `"${j.table}" AS "${j.alias}"`).join(', ');
1583
- sql += ` FROM ${fromTables}`;
1584
- const joinConditions = this.buildJoinConditions(joinInfos);
1585
- let whereClause = joinConditions;
1586
- if (filterText) {
1587
- whereClause = whereClause ? `${whereClause} AND ${filterText}` : filterText;
1588
- }
1589
- if (whereClause) {
1590
- sql += ` WHERE ${whereClause}`;
1591
- }
1592
- }
1593
- return sql;
1594
- }
1595
- }
1596
- /**
1597
- * PostgreSQL专用的结构化JOIN分析
1598
- * 返回结构化的JOIN信息,而不是拼接好的FROM字符串
1599
- */
1600
- analyzeJoinStructured(entity, { filter, sorter }, initialNumber) {
1601
- const { schema } = this;
1602
- let number = initialNumber || 1;
1603
- const filterRefAlias = {};
1604
- const mainAlias = `${entity}_${number++}`;
1605
- const mainTable = this.getStorageName(entity);
1606
- const aliasDict = {
1607
- './': mainAlias,
1608
- };
1609
- const joinInfos = [];
1610
- const analyzeFilterNode = ({ node, path, entityName, alias }) => {
1611
- Object.keys(node).forEach((op) => {
1612
- if (['$and', '$or'].includes(op)) {
1613
- node[op].forEach((subNode) => analyzeFilterNode({
1614
- node: subNode,
1615
- path,
1616
- entityName,
1617
- alias,
1618
- }));
1619
- }
1620
- else if (['$not'].includes(op)) {
1621
- analyzeFilterNode({
1622
- node: node[op],
1623
- path,
1624
- entityName,
1625
- alias,
1626
- });
1627
- }
1628
- else if (['$text'].includes(op)) {
1629
- // 全文搜索,不需要JOIN
1630
- }
1631
- else {
1632
- const rel = (0, relation_1.judgeRelation)(this.schema, entityName, op);
1633
- if (typeof rel === 'string') {
1634
- const pathAttr = `${path}${op}/`;
1635
- if (!aliasDict.hasOwnProperty(pathAttr)) {
1636
- const alias2 = `${rel}_${number++}`;
1637
- aliasDict[pathAttr] = alias2;
1638
- joinInfos.push({
1639
- table: this.getStorageName(rel),
1640
- alias: alias2,
1641
- leftAlias: alias,
1642
- leftKey: op + 'Id',
1643
- rightKey: 'id',
1644
- });
1645
- analyzeFilterNode({
1646
- node: node[op],
1647
- path: pathAttr,
1648
- entityName: rel,
1649
- alias: alias2,
1650
- });
1651
- }
1652
- else {
1653
- analyzeFilterNode({
1654
- node: node[op],
1655
- path: pathAttr,
1656
- entityName: rel,
1657
- alias: aliasDict[pathAttr],
1658
- });
1659
- }
1660
- }
1661
- else if (rel === 2) {
1662
- const pathAttr = `${path}${op}/`;
1663
- if (!aliasDict.hasOwnProperty(pathAttr)) {
1664
- const alias2 = `${op}_${number++}`;
1665
- aliasDict[pathAttr] = alias2;
1666
- joinInfos.push({
1667
- table: this.getStorageName(op),
1668
- alias: alias2,
1669
- leftAlias: alias,
1670
- leftKey: 'entityId',
1671
- rightKey: 'id',
1672
- extraCondition: `"${alias}"."entity" = '${op}'`,
1673
- });
1674
- analyzeFilterNode({
1675
- node: node[op],
1676
- path: pathAttr,
1677
- entityName: op,
1678
- alias: alias2,
1679
- });
1680
- }
1681
- else {
1682
- analyzeFilterNode({
1683
- node: node[op],
1684
- path: pathAttr,
1685
- entityName: op,
1686
- alias: aliasDict[pathAttr],
1687
- });
1688
- }
1689
- }
1690
- }
1691
- });
1692
- if (node['#id']) {
1693
- (0, assert_1.default)(!filterRefAlias[node['#id']]);
1694
- filterRefAlias[node['#id']] = [alias, entityName];
1695
- }
1696
- };
1697
- if (filter) {
1698
- analyzeFilterNode({
1699
- node: filter,
1700
- path: './',
1701
- entityName: entity,
1702
- alias: mainAlias,
1703
- });
1704
- }
1705
- // TODO: sorter的分析类似,这里省略(UPDATE/DELETE通常不需要sorter)
1706
- (0, assert_1.default)(!sorter, '当前analyzeJoinStructured不支持sorter行为');
1707
- return {
1708
- aliasDict,
1709
- filterRefAlias,
1710
- mainTable,
1711
- mainAlias,
1712
- joinInfos,
1713
- currentNumber: number,
1714
- };
1715
- }
1716
- /**
1717
- * 构建JOIN条件(用于UPDATE/DELETE的WHERE子句)
1718
- */
1719
- buildJoinConditions(joinInfos) {
1720
- const conditions = [];
1721
- for (const join of joinInfos) {
1722
- let condition = `"${join.leftAlias}"."${join.leftKey}" = "${join.alias}"."${join.rightKey}"`;
1723
- if (join.extraCondition) {
1724
- condition = `(${condition} AND ${join.extraCondition})`;
1725
- }
1726
- conditions.push(condition);
1591
+ sql = `UPDATE "${mainTable}" AS "${mainAlias}" SET ${setClause} WHERE "${types_1.PrimaryKeyAttribute}" IN (${subquery})`;
1727
1592
  }
1728
- return conditions.join(' AND ');
1593
+ // 添加 RETURNING 子句
1594
+ return this.appendReturningClause(sql, entity, mainAlias, option);
1729
1595
  }
1730
1596
  /**
1731
1597
  * 生成 PostgreSQL UPSERT 语句
@@ -53,7 +53,11 @@ export declare abstract class SqlTranslator<ED extends EntityDict & BaseEntityDi
53
53
  };
54
54
  private translateSorter;
55
55
  private translateProjection;
56
- private translateSelectInner;
56
+ protected translateSelectInner<T extends keyof ED, OP extends SqlSelectOption>(entity: T, selection: ED[T]['Selection'], initialNumber: number, refAlias: Record<string, [string, keyof ED]>, option?: OP): {
57
+ filterStmt: string;
58
+ stmt: string;
59
+ currentNumber: number;
60
+ };
57
61
  translateSelect<T extends keyof ED, OP extends SqlSelectOption>(entity: T, selection: ED[T]['Selection'], option?: OP): string;
58
62
  translateWhere<T extends keyof ED, OP extends SqlSelectOption>(entity: T, selection: ED[T]['Selection'], option?: OP): string;
59
63
  translateAggregate<T extends keyof ED, OP extends SqlSelectOption>(entity: T, aggregation: ED[T]['Aggregation'], option?: OP): string;
@@ -564,10 +564,10 @@ class SqlTranslator {
564
564
  translateFilter(entity, filter, aliasDict, filterRefAlias, initialNumber, option) {
565
565
  const { schema } = this;
566
566
  let currentNumber = initialNumber;
567
- const translateInner = (entity2, path, filter2, type) => {
567
+ const translateInner = (entity2, path, filter2, skipDeletePhy) => {
568
568
  const alias = aliasDict[path];
569
569
  const { attributes } = schema[entity2];
570
- let whereText = type ? '' : this.getDefaultSelectFilter(alias, option);
570
+ let whereText = skipDeletePhy ? '' : this.getDefaultSelectFilter(alias, option);
571
571
  if (filter2) {
572
572
  const attrs = Object.keys(filter2).filter(ele => !ele.startsWith('#'));
573
573
  attrs.forEach((attr) => {
@@ -582,7 +582,7 @@ class SqlTranslator {
582
582
  case '$xor': {
583
583
  const logicQueries = filter2[attr];
584
584
  logicQueries.forEach((logicQuery, index) => {
585
- const sql = translateInner(entity2, path, logicQuery, 'ref'); // 只要传个值就行了,应该无所谓
585
+ const sql = translateInner(entity2, path, logicQuery, true); // 只要传个值就行了,应该无所谓
586
586
  if (sql) {
587
587
  whereText += ` (${sql})`;
588
588
  if (index < logicQueries.length - 1) {
@@ -595,7 +595,7 @@ class SqlTranslator {
595
595
  default: {
596
596
  (0, assert_1.default)(attr === '$not');
597
597
  const logicQuery = filter2[attr];
598
- const sql = translateInner(entity2, path, logicQuery, 'ref'); // 只要传个值就行了,应该无所谓
598
+ const sql = translateInner(entity2, path, logicQuery, true); // 只要传个值就行了,应该无所谓
599
599
  if (sql) {
600
600
  whereText += ` not (${sql})`;
601
601
  break;
@@ -965,6 +965,10 @@ class SqlTranslator {
965
965
  const { data, filter, sorter, indexFrom, count } = operation;
966
966
  (0, assert_1.default)(!sorter, '当前remove不支持sorter行为');
967
967
  const { aliasDict, filterRefAlias, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
968
+ // 对于limit,如果有indexForm则一定有count
969
+ if (typeof indexFrom === 'number') {
970
+ (0, assert_1.default)(typeof count === 'number' && count > 0);
971
+ }
968
972
  const alias = aliasDict['./'];
969
973
  // 这里原来includeDeleted传的是true,不知道原因,但不合理
970
974
  const { stmt: filterText } = this.translateFilter(entity, filter, aliasDict, filterRefAlias, currentNumber, { includedDeleted: option?.includedDeleted });
@@ -992,6 +996,10 @@ class SqlTranslator {
992
996
  const { filter, sorter, indexFrom, count, data } = operation;
993
997
  (0, assert_1.default)(!sorter, '当前update不支持sorter行为');
994
998
  const { aliasDict, filterRefAlias, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
999
+ // 对于limit,如果有indexForm则一定有count
1000
+ if (typeof indexFrom === 'number') {
1001
+ (0, assert_1.default)(typeof count === 'number' && count > 0);
1002
+ }
995
1003
  const alias = aliasDict['./'];
996
1004
  let updateText = '';
997
1005
  for (const attr in data) {
@@ -16,6 +16,7 @@ export type Plan = {
16
16
  updatedIndexes: Record<string, Index<any>[]>;
17
17
  };
18
18
  export interface DbStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends AsyncRowStore<ED, Cxt> {
19
+ checkRelationAsync<T extends keyof ED, Cxt extends AsyncContext<ED>>(entity: T, operation: Omit<ED[T]['Operation'] | ED[T]['Selection'], 'id'>, context: Cxt): Promise<void>;
19
20
  connect: () => Promise<void>;
20
21
  disconnect: () => Promise<void>;
21
22
  initialize(options: CreateEntityOption): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oak-db",
3
- "version": "3.3.13",
3
+ "version": "3.3.14",
4
4
  "description": "oak-db",
5
5
  "main": "lib/index",
6
6
  "author": {
@@ -18,7 +18,7 @@
18
18
  "lodash": "^4.17.21",
19
19
  "mysql": "^2.18.1",
20
20
  "mysql2": "^2.3.3",
21
- "oak-domain": "^5.1.34",
21
+ "oak-domain": "^5.1.36",
22
22
  "pg": "^8.16.3",
23
23
  "uuid": "^8.3.2"
24
24
  },