oak-db 3.3.12 → 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.
- package/lib/MySQL/connector.js +9 -14
- package/lib/MySQL/store.d.ts +5 -15
- package/lib/MySQL/store.js +11 -2
- package/lib/MySQL/translator.js +22 -11
- package/lib/PostgreSQL/connector.d.ts +0 -4
- package/lib/PostgreSQL/connector.js +29 -58
- package/lib/PostgreSQL/store.d.ts +16 -1
- package/lib/PostgreSQL/store.js +247 -7
- package/lib/PostgreSQL/translator.d.ts +39 -7
- package/lib/PostgreSQL/translator.js +480 -225
- package/lib/sqlTranslator.d.ts +5 -1
- package/lib/sqlTranslator.js +18 -8
- package/lib/types/dbStore.d.ts +27 -2
- package/package.json +2 -2
package/lib/MySQL/connector.js
CHANGED
|
@@ -20,27 +20,22 @@ class MySqlConnector {
|
|
|
20
20
|
return this.pool.end();
|
|
21
21
|
}
|
|
22
22
|
async startTransaction(option) {
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
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
|
-
|
|
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') {
|
package/lib/MySQL/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult
|
|
1
|
+
import { EntityDict, OperateOption, OperationResult, TxnOption, StorageSchema, SelectOption, AggregationResult } from 'oak-domain/lib/types';
|
|
2
2
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
3
|
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
|
4
4
|
import { MySQLConfiguration } from './types/Configuration';
|
|
@@ -7,7 +7,7 @@ import { MySqlTranslator, MySqlSelectOption, MysqlOperateOption } from './transl
|
|
|
7
7
|
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
|
-
import { DbStore } from '../types/dbStore';
|
|
10
|
+
import { DbStore, Plan } from '../types/dbStore';
|
|
11
11
|
export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
|
12
12
|
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
13
|
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']>;
|
|
@@ -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>;
|
|
@@ -48,16 +51,3 @@ export declare class MysqlStore<ED extends EntityDict & BaseEntityDict, Cxt exte
|
|
|
48
51
|
*/
|
|
49
52
|
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
|
|
50
53
|
}
|
|
51
|
-
type Plan = {
|
|
52
|
-
newTables: Record<string, {
|
|
53
|
-
attributes: Record<string, Attribute>;
|
|
54
|
-
}>;
|
|
55
|
-
newIndexes: Record<string, Index<any>[]>;
|
|
56
|
-
updatedTables: Record<string, {
|
|
57
|
-
attributes: Record<string, Attribute & {
|
|
58
|
-
isNew: boolean;
|
|
59
|
-
}>;
|
|
60
|
-
}>;
|
|
61
|
-
updatedIndexes: Record<string, Index<any>[]>;
|
|
62
|
-
};
|
|
63
|
-
export {};
|
package/lib/MySQL/store.js
CHANGED
|
@@ -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].
|
|
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');
|
|
@@ -354,7 +363,7 @@ class MysqlStore extends CascadeStore_1.CascadeStore {
|
|
|
354
363
|
};
|
|
355
364
|
for (const attr in attributesNew) {
|
|
356
365
|
if (attributes[attr]) {
|
|
357
|
-
// 因为反向无法复原原来定义的attribute类型,这里就比较两次创建的sql
|
|
366
|
+
// 因为反向无法复原原来定义的attribute类型,这里就比较两次创建的sql是不是一致,不是太好的设计。
|
|
358
367
|
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
|
|
359
368
|
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
|
|
360
369
|
if (!this.translator.compareSql(sql1, sql2)) {
|
package/lib/MySQL/translator.js
CHANGED
|
@@ -872,8 +872,10 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
872
872
|
const refId = (expr)['#refId'];
|
|
873
873
|
const refAttr = (expr)['#refAttr'];
|
|
874
874
|
(0, assert_1.default)(refDict[refId]);
|
|
875
|
-
const
|
|
876
|
-
|
|
875
|
+
const [refAlias, refEntity] = refDict[refId];
|
|
876
|
+
const attrText = `\`${refAlias}\`.\`${refAttr}\``;
|
|
877
|
+
// 这里必须使用refEntity,否则在filter深层嵌套节点表达式时会出现entity不对应
|
|
878
|
+
result = this.translateAttrInExpression(refEntity, (expr)['#refAttr'], attrText);
|
|
877
879
|
}
|
|
878
880
|
else {
|
|
879
881
|
(0, assert_1.default)(k.length === 1);
|
|
@@ -1067,9 +1069,12 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1067
1069
|
if (sorterText) {
|
|
1068
1070
|
sql += ` order by ${sorterText}`;
|
|
1069
1071
|
}
|
|
1070
|
-
if (typeof
|
|
1071
|
-
|
|
1072
|
-
|
|
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}`;
|
|
1073
1078
|
}
|
|
1074
1079
|
return sql;
|
|
1075
1080
|
}
|
|
@@ -1085,9 +1090,12 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1085
1090
|
if (sorterText) {
|
|
1086
1091
|
sql += ` order by ${sorterText}`;
|
|
1087
1092
|
}
|
|
1088
|
-
if (typeof
|
|
1089
|
-
|
|
1090
|
-
|
|
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}`;
|
|
1091
1099
|
}
|
|
1092
1100
|
return sql;
|
|
1093
1101
|
}
|
|
@@ -1103,9 +1111,12 @@ class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
|
|
|
1103
1111
|
if (sorterText) {
|
|
1104
1112
|
sql += ` order by ${sorterText}`;
|
|
1105
1113
|
}
|
|
1106
|
-
if (typeof
|
|
1107
|
-
|
|
1108
|
-
|
|
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}`;
|
|
1109
1120
|
}
|
|
1110
1121
|
return sql;
|
|
1111
1122
|
}
|
|
@@ -16,10 +16,6 @@ export declare class PostgreSQLConnector {
|
|
|
16
16
|
exec(sql: string, txn?: string): Promise<[QueryResultRow[], QueryResult]>;
|
|
17
17
|
commitTransaction(txn: string): Promise<void>;
|
|
18
18
|
rollbackTransaction(txn: string): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* 执行多条 SQL 语句(用于初始化等场景)
|
|
21
|
-
*/
|
|
22
|
-
execBatch(sqls: string[], txn?: string): Promise<void>;
|
|
23
19
|
/**
|
|
24
20
|
* 获取连接池状态
|
|
25
21
|
*/
|
|
@@ -36,37 +36,27 @@ class PostgreSQLConnector {
|
|
|
36
36
|
await this.pool.end();
|
|
37
37
|
}
|
|
38
38
|
async startTransaction(option) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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];
|
|
@@ -133,16 +114,6 @@ class PostgreSQLConnector {
|
|
|
133
114
|
connection.release();
|
|
134
115
|
}
|
|
135
116
|
}
|
|
136
|
-
/**
|
|
137
|
-
* 执行多条 SQL 语句(用于初始化等场景)
|
|
138
|
-
*/
|
|
139
|
-
async execBatch(sqls, txn) {
|
|
140
|
-
for (const sql of sqls) {
|
|
141
|
-
if (sql.trim()) {
|
|
142
|
-
await this.exec(sql, txn);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
117
|
/**
|
|
147
118
|
* 获取连接池状态
|
|
148
119
|
*/
|
|
@@ -7,7 +7,7 @@ import { PostgreSQLTranslator, PostgreSQLSelectOption, PostgreSQLOperateOption }
|
|
|
7
7
|
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
|
-
import { DbStore } from '../types/dbStore';
|
|
10
|
+
import { DbStore, Plan } from '../types/dbStore';
|
|
11
11
|
export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>> extends CascadeStore<ED> implements DbStore<ED, Cxt> {
|
|
12
12
|
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
13
|
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']>;
|
|
@@ -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>;
|
|
@@ -35,4 +38,16 @@ export declare class PostgreSQLStore<ED extends EntityDict & BaseEntityDict, Cxt
|
|
|
35
38
|
connect(): Promise<void>;
|
|
36
39
|
disconnect(): Promise<void>;
|
|
37
40
|
initialize(option: CreateEntityOption): Promise<void>;
|
|
41
|
+
readSchema(): Promise<StorageSchema<ED>>;
|
|
42
|
+
/**
|
|
43
|
+
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
|
44
|
+
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
|
45
|
+
*/
|
|
46
|
+
makeUpgradePlan(): Promise<Plan>;
|
|
47
|
+
/**
|
|
48
|
+
* 比较两个schema的不同,这里计算的是new对old的增量
|
|
49
|
+
* @param schemaOld
|
|
50
|
+
* @param schemaNew
|
|
51
|
+
*/
|
|
52
|
+
diffSchema(schemaOld: StorageSchema<any>, schemaNew: StorageSchema<any>): Plan;
|
|
38
53
|
}
|
package/lib/PostgreSQL/store.js
CHANGED
|
@@ -8,6 +8,12 @@ const translator_1 = require("./translator");
|
|
|
8
8
|
const lodash_1 = require("lodash");
|
|
9
9
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
10
10
|
const relation_1 = require("oak-domain/lib/store/relation");
|
|
11
|
+
const ToNumberAttrs = new Set([
|
|
12
|
+
'$$seq$$',
|
|
13
|
+
'$$createAt$$',
|
|
14
|
+
'$$updateAt$$',
|
|
15
|
+
'$$deleteAt$$',
|
|
16
|
+
]);
|
|
11
17
|
function convertGeoTextToObject(geoText) {
|
|
12
18
|
if (geoText.startsWith('POINT')) {
|
|
13
19
|
const coord = geoText.match(/(-?\d+\.?\d*)/g);
|
|
@@ -88,6 +94,9 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
88
94
|
supportMultipleCreate() {
|
|
89
95
|
return true;
|
|
90
96
|
}
|
|
97
|
+
supportUpdateReturning() {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
91
100
|
formResult(entity, result) {
|
|
92
101
|
const schema = this.getSchema();
|
|
93
102
|
function resolveAttribute(entity2, r, attr, value) {
|
|
@@ -121,12 +130,19 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
121
130
|
const { type } = attributes[attr];
|
|
122
131
|
switch (type) {
|
|
123
132
|
case 'date':
|
|
124
|
-
case 'time':
|
|
133
|
+
case 'time':
|
|
134
|
+
case 'datetime': {
|
|
125
135
|
if (value instanceof Date) {
|
|
126
136
|
r[attr] = value.valueOf();
|
|
127
137
|
}
|
|
128
138
|
else {
|
|
129
|
-
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
r[attr] = parseInt(value, 10);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
(0, assert_1.default)(typeof value === 'number' || value === null);
|
|
144
|
+
r[attr] = value;
|
|
145
|
+
}
|
|
130
146
|
}
|
|
131
147
|
break;
|
|
132
148
|
}
|
|
@@ -203,6 +219,14 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
203
219
|
// PostgreSQL count 返回字符串
|
|
204
220
|
r[attr] = parseInt(value, 10);
|
|
205
221
|
}
|
|
222
|
+
else if (attr.startsWith("#sum") || attr.startsWith("#avg") || attr.startsWith("#min") || attr.startsWith("#max")) {
|
|
223
|
+
// PostgreSQL sum/avg/min/max 返回字符串
|
|
224
|
+
r[attr] = parseFloat(value);
|
|
225
|
+
}
|
|
226
|
+
else if (ToNumberAttrs.has(attr)) {
|
|
227
|
+
// PostgreSQL sum/avg/min/max 返回字符串
|
|
228
|
+
r[attr] = parseInt(value, 10);
|
|
229
|
+
}
|
|
206
230
|
else {
|
|
207
231
|
r[attr] = value;
|
|
208
232
|
}
|
|
@@ -281,6 +305,33 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
281
305
|
}
|
|
282
306
|
}
|
|
283
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
|
+
}
|
|
284
335
|
async operate(entity, operation, context, option) {
|
|
285
336
|
const { action } = operation;
|
|
286
337
|
(0, assert_1.default)(!['select', 'download', 'stat'].includes(action), '不支持使用 select operation');
|
|
@@ -316,14 +367,203 @@ class PostgreSQLStore extends CascadeStore_1.CascadeStore {
|
|
|
316
367
|
}
|
|
317
368
|
async initialize(option) {
|
|
318
369
|
const schema = this.getSchema();
|
|
319
|
-
//
|
|
320
|
-
|
|
370
|
+
// ===== 第一阶段:事务外创建扩展 =====
|
|
371
|
+
let hasGeoType = false;
|
|
372
|
+
let hasChineseTsConfig = false;
|
|
373
|
+
let chineseParser = null;
|
|
374
|
+
// 扫描 schema
|
|
321
375
|
for (const entity in schema) {
|
|
322
|
-
const
|
|
323
|
-
for (const
|
|
324
|
-
|
|
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
|
+
// ===== 第二阶段:事务内创建配置和表 =====
|
|
402
|
+
const txn = await this.connector.startTransaction({
|
|
403
|
+
isolationLevel: 'serializable',
|
|
404
|
+
});
|
|
405
|
+
try {
|
|
406
|
+
// 创建中文文本搜索配置
|
|
407
|
+
if (hasChineseTsConfig) {
|
|
408
|
+
console.log('Initializing Chinese text search configuration...');
|
|
409
|
+
const checkChineseConfigSql = `
|
|
410
|
+
SELECT COUNT(*) as cnt
|
|
411
|
+
FROM pg_catalog.pg_ts_config
|
|
412
|
+
WHERE cfgname = 'chinese';
|
|
413
|
+
`;
|
|
414
|
+
const result = await this.connector.exec(checkChineseConfigSql, txn);
|
|
415
|
+
const count = parseInt(result[0][0]?.cnt || '0', 10);
|
|
416
|
+
if (count === 0) {
|
|
417
|
+
const createChineseConfigSql = `
|
|
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;
|
|
420
|
+
`;
|
|
421
|
+
await this.connector.exec(createChineseConfigSql, txn);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// 创建实体表
|
|
425
|
+
for (const entity in schema) {
|
|
426
|
+
const sqls = this.translator.translateCreateEntity(entity, option);
|
|
427
|
+
for (const sql of sqls) {
|
|
428
|
+
await this.connector.exec(sql, txn);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await this.connector.commitTransaction(txn);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
await this.connector.rollbackTransaction(txn);
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// 从数据库中读取当前schema
|
|
439
|
+
readSchema() {
|
|
440
|
+
return this.translator.readSchema((sql) => this.connector.exec(sql));
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 根据载入的dataSchema,和数据库中原来的schema,决定如何来upgrade
|
|
444
|
+
* 制订出来的plan分为两阶段:增加阶段和削减阶段,在两个阶段之间,由用户来修正数据
|
|
445
|
+
*/
|
|
446
|
+
async makeUpgradePlan() {
|
|
447
|
+
const originSchema = await this.readSchema();
|
|
448
|
+
const plan = this.diffSchema(originSchema, this.translator.schema);
|
|
449
|
+
return plan;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 比较两个schema的不同,这里计算的是new对old的增量
|
|
453
|
+
* @param schemaOld
|
|
454
|
+
* @param schemaNew
|
|
455
|
+
*/
|
|
456
|
+
diffSchema(schemaOld, schemaNew) {
|
|
457
|
+
const plan = {
|
|
458
|
+
newTables: {},
|
|
459
|
+
newIndexes: {},
|
|
460
|
+
updatedIndexes: {},
|
|
461
|
+
updatedTables: {},
|
|
462
|
+
};
|
|
463
|
+
for (const table in schemaNew) {
|
|
464
|
+
// PostgreSQL 表名区分大小写(使用双引号时)
|
|
465
|
+
if (schemaOld[table]) {
|
|
466
|
+
const { attributes, indexes } = schemaOld[table];
|
|
467
|
+
const { attributes: attributesNew, indexes: indexesNew } = schemaNew[table];
|
|
468
|
+
const assignToUpdateTables = (attr, isNew) => {
|
|
469
|
+
const skipAttrs = ['$$seq$$', '$$createAt$$', '$$updateAt$$', '$$deleteAt$$', 'id'];
|
|
470
|
+
if (skipAttrs.includes(attr)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!plan.updatedTables[table]) {
|
|
474
|
+
plan.updatedTables[table] = {
|
|
475
|
+
attributes: {
|
|
476
|
+
[attr]: {
|
|
477
|
+
...attributesNew[attr],
|
|
478
|
+
isNew,
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
plan.updatedTables[table].attributes[attr] = {
|
|
485
|
+
...attributesNew[attr],
|
|
486
|
+
isNew,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
for (const attr in attributesNew) {
|
|
491
|
+
if (attributes[attr]) {
|
|
492
|
+
// 比较两次创建的属性定义是否一致
|
|
493
|
+
const sql1 = this.translator.translateAttributeDef(attr, attributesNew[attr]);
|
|
494
|
+
const sql2 = this.translator.translateAttributeDef(attr, attributes[attr]);
|
|
495
|
+
if (!this.translator.compareSql(sql1, sql2)) {
|
|
496
|
+
assignToUpdateTables(attr, false);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
assignToUpdateTables(attr, true);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (indexesNew) {
|
|
504
|
+
const assignToIndexes = (index, isNew) => {
|
|
505
|
+
if (isNew) {
|
|
506
|
+
if (plan.newIndexes[table]) {
|
|
507
|
+
plan.newIndexes[table].push(index);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
plan.newIndexes[table] = [index];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
if (plan.updatedIndexes[table]) {
|
|
515
|
+
plan.updatedIndexes[table].push(index);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
plan.updatedIndexes[table] = [index];
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const compareConfig = (config1, config2) => {
|
|
523
|
+
const unique1 = config1?.unique || false;
|
|
524
|
+
const unique2 = config2?.unique || false;
|
|
525
|
+
if (unique1 !== unique2) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
const type1 = config1?.type || 'btree';
|
|
529
|
+
const type2 = config2?.type || 'btree';
|
|
530
|
+
// tsConfig 比较
|
|
531
|
+
const tsConfig1 = config1?.tsConfig;
|
|
532
|
+
const tsConfig2 = config2?.tsConfig;
|
|
533
|
+
if (JSON.stringify(tsConfig1) !== JSON.stringify(tsConfig2)) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
return type1 === type2;
|
|
537
|
+
};
|
|
538
|
+
for (const index of indexesNew) {
|
|
539
|
+
const { name, config, attributes: indexAttrs } = index;
|
|
540
|
+
const origin = indexes?.find(ele => ele.name === name);
|
|
541
|
+
if (origin) {
|
|
542
|
+
if (JSON.stringify(indexAttrs) !== JSON.stringify(origin.attributes)) {
|
|
543
|
+
assignToIndexes(index, false);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
if (!compareConfig(config, origin.config)) {
|
|
547
|
+
assignToIndexes(index, false);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
assignToIndexes(index, true);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
plan.newTables[table] = {
|
|
559
|
+
attributes: schemaNew[table].attributes,
|
|
560
|
+
};
|
|
561
|
+
if (schemaNew[table].indexes) {
|
|
562
|
+
plan.newIndexes[table] = schemaNew[table].indexes;
|
|
563
|
+
}
|
|
325
564
|
}
|
|
326
565
|
}
|
|
566
|
+
return plan;
|
|
327
567
|
}
|
|
328
568
|
}
|
|
329
569
|
exports.PostgreSQLStore = PostgreSQLStore;
|