fdb2 1.0.37 → 1.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/bin/fdb2.js +78 -3
- package/dist/public/explorer.css +517 -478
- package/dist/public/explorer.js +1114 -872
- package/dist/public/index.css +87 -27
- package/dist/public/index.js +97 -49
- package/dist/public/layout.js +2 -2
- package/dist/public/vue.js +1 -1
- package/dist/server/core/schema/__tests__/canonical.test.ts +195 -0
- package/dist/server/core/schema/__tests__/ddl-emitter.test.ts +456 -0
- package/dist/server/core/schema/__tests__/diff.test.ts +242 -0
- package/dist/server/core/schema/__tests__/roundtrip.property.test.ts +403 -0
- package/dist/server/core/schema/canonical.d.ts +50 -0
- package/dist/server/core/schema/canonical.d.ts.map +1 -0
- package/dist/server/core/schema/canonical.js +212 -0
- package/dist/server/core/schema/canonical.js.map +1 -0
- package/dist/server/core/schema/canonical.ts +223 -0
- package/dist/server/core/schema/ddl-emitter.d.ts +68 -0
- package/dist/server/core/schema/ddl-emitter.d.ts.map +1 -0
- package/dist/server/core/schema/ddl-emitter.js +813 -0
- package/dist/server/core/schema/ddl-emitter.js.map +1 -0
- package/dist/server/core/schema/ddl-emitter.ts +925 -0
- package/dist/server/core/schema/diff.d.ts +16 -0
- package/dist/server/core/schema/diff.d.ts.map +1 -0
- package/dist/server/core/schema/diff.js +115 -0
- package/dist/server/core/schema/diff.js.map +1 -0
- package/dist/server/core/schema/diff.ts +134 -0
- package/dist/server/core/schema/type-defaults.d.ts +27 -0
- package/dist/server/core/schema/type-defaults.d.ts.map +1 -0
- package/dist/server/core/schema/type-defaults.js +61 -0
- package/dist/server/core/schema/type-defaults.js.map +1 -0
- package/dist/server/core/schema/type-defaults.ts +79 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +347 -124
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.ts +336 -161
- package/dist/server/model/ddl-spec.d.ts +163 -0
- package/dist/server/model/ddl-spec.d.ts.map +1 -0
- package/dist/server/model/ddl-spec.js +32 -0
- package/dist/server/model/ddl-spec.js.map +1 -0
- package/dist/server/model/ddl-spec.ts +175 -0
- package/dist/server/service/connection.service.d.ts +20 -0
- package/dist/server/service/connection.service.d.ts.map +1 -1
- package/dist/server/service/connection.service.js +99 -25
- package/dist/server/service/connection.service.js.map +1 -1
- package/dist/server/service/connection.service.ts +104 -25
- package/dist/server/service/database/base.service.d.ts +0 -4
- package/dist/server/service/database/base.service.d.ts.map +1 -1
- package/dist/server/service/database/base.service.js.map +1 -1
- package/dist/server/service/database/base.service.ts +0 -5
- package/dist/server/service/database/clickhouse.service.d.ts +0 -5
- package/dist/server/service/database/clickhouse.service.d.ts.map +1 -1
- package/dist/server/service/database/clickhouse.service.js +0 -49
- package/dist/server/service/database/clickhouse.service.js.map +1 -1
- package/dist/server/service/database/clickhouse.service.ts +0 -52
- package/dist/server/service/database/cockroachdb.service.d.ts +0 -5
- package/dist/server/service/database/cockroachdb.service.d.ts.map +1 -1
- package/dist/server/service/database/cockroachdb.service.js +11 -142
- package/dist/server/service/database/cockroachdb.service.js.map +1 -1
- package/dist/server/service/database/cockroachdb.service.ts +11 -161
- package/dist/server/service/database/database.service.d.ts +55 -2
- package/dist/server/service/database/database.service.d.ts.map +1 -1
- package/dist/server/service/database/database.service.js +246 -4
- package/dist/server/service/database/database.service.js.map +1 -1
- package/dist/server/service/database/database.service.ts +295 -7
- package/dist/server/service/database/mongodb.service.d.ts +0 -6
- package/dist/server/service/database/mongodb.service.d.ts.map +1 -1
- package/dist/server/service/database/mongodb.service.js +0 -8
- package/dist/server/service/database/mongodb.service.js.map +1 -1
- package/dist/server/service/database/mongodb.service.ts +0 -8
- package/dist/server/service/database/mssql.service.d.ts +8 -4
- package/dist/server/service/database/mssql.service.d.ts.map +1 -1
- package/dist/server/service/database/mssql.service.js +27 -101
- package/dist/server/service/database/mssql.service.js.map +1 -1
- package/dist/server/service/database/mssql.service.ts +28 -122
- package/dist/server/service/database/mysql.service.d.ts +0 -4
- package/dist/server/service/database/mysql.service.d.ts.map +1 -1
- package/dist/server/service/database/mysql.service.js +11 -137
- package/dist/server/service/database/mysql.service.js.map +1 -1
- package/dist/server/service/database/mysql.service.ts +11 -173
- package/dist/server/service/database/oracle.service.d.ts +0 -4
- package/dist/server/service/database/oracle.service.d.ts.map +1 -1
- package/dist/server/service/database/oracle.service.js +11 -104
- package/dist/server/service/database/oracle.service.js.map +1 -1
- package/dist/server/service/database/oracle.service.ts +11 -127
- package/dist/server/service/database/postgres.service.d.ts +0 -4
- package/dist/server/service/database/postgres.service.d.ts.map +1 -1
- package/dist/server/service/database/postgres.service.js +11 -112
- package/dist/server/service/database/postgres.service.js.map +1 -1
- package/dist/server/service/database/postgres.service.ts +11 -133
- package/dist/server/service/database/sap.service.d.ts +0 -4
- package/dist/server/service/database/sap.service.d.ts.map +1 -1
- package/dist/server/service/database/sap.service.js +11 -137
- package/dist/server/service/database/sap.service.js.map +1 -1
- package/dist/server/service/database/sap.service.ts +11 -158
- package/dist/server/service/database/sql-type.helper.d.ts.map +1 -1
- package/dist/server/service/database/sql-type.helper.js +9 -47
- package/dist/server/service/database/sql-type.helper.js.map +1 -1
- package/dist/server/service/database/sql-type.helper.ts +11 -53
- package/dist/server/service/database/sqlite.service.d.ts +0 -5
- package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
- package/dist/server/service/database/sqlite.service.js +11 -147
- package/dist/server/service/database/sqlite.service.js.map +1 -1
- package/dist/server/service/database/sqlite.service.ts +11 -177
- package/package.json +1 -1
- package/server.js +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DDL 契约(单一事实来源)
|
|
3
|
+
*
|
|
4
|
+
* 本文件是「前端表单 <-> 后端 schema 引擎」之间唯一的类型契约,
|
|
5
|
+
* 前端直接 import 本文件(前端 tsconfig 已包含 server 目录),禁止再复制一份。
|
|
6
|
+
*
|
|
7
|
+
* 核心不变式(由 server/core/schema 保证,测试钉死):
|
|
8
|
+
* 1. 纯函数性:ChangeSet = f(canonical(原始态), canonical(目标态)),与操作路径无关;
|
|
9
|
+
* 任何"改了又改回"的 UI 操作序列,diff 结果恒为空(幂等)。
|
|
10
|
+
* 2. 单归属:每个语义维度只出现在一个桶里——
|
|
11
|
+
* 列定义在 modified,主键在 primaryKey(列上不存在 isPrimary),改名在 renamed。
|
|
12
|
+
* 3. 归一化后比较:两侧先投影为 CanonicalTable 再 diff,
|
|
13
|
+
* 表示差异(''/null/0、大小写、空格、'varchar(255)' vs {type,length})不产生假 diff。
|
|
14
|
+
*
|
|
15
|
+
* 注意:ColumnSpec 有 hasDefault 布尔,解决 ColumnEntity.defaultValue?: any 的三态歧义
|
|
16
|
+
* (无法区分「无默认值」/「DEFAULT NULL」/「DEFAULT 0」/「DEFAULT ''」)。
|
|
17
|
+
*/
|
|
18
|
+
/** 默认值语义类别(归一化后的三态+表达式分类) */
|
|
19
|
+
export type DefaultKind = 'none' | 'null' | 'literal' | 'expression';
|
|
20
|
+
/**
|
|
21
|
+
* 目标列定义(前端表单 -> 后端)
|
|
22
|
+
* 前端负责把 UI 状态整理成该结构;不发送 UI 哨兵值(如 '__custom__'),
|
|
23
|
+
* 自定义类型直接放 customType。
|
|
24
|
+
*/
|
|
25
|
+
export interface ColumnSpec {
|
|
26
|
+
/** 列名(原样,不去大小写化——标识符匹配按精确名) */
|
|
27
|
+
name: string;
|
|
28
|
+
/** 基础类型名,如 VARCHAR / DECIMAL(大小写均可,后端归一为大写比较) */
|
|
29
|
+
type: string;
|
|
30
|
+
/** 完整自定义类型串,如 ENUM('a','b')、DECIMAL(20,0);非空时忽略 type/length/precision/scale */
|
|
31
|
+
customType?: string;
|
|
32
|
+
/** 字符长度(string/number 均可,''/null 归一为未设置) */
|
|
33
|
+
length?: number | string | null;
|
|
34
|
+
/** 数值精度(precision=总位数;scale=小数位数。注意与 scale 区分) */
|
|
35
|
+
precision?: number | string | null;
|
|
36
|
+
/** 小数位数(DECIMAL(10,0) 的 0 必须保留,不得因假值被丢弃) */
|
|
37
|
+
scale?: number | string | null;
|
|
38
|
+
/** 是否允许 NULL */
|
|
39
|
+
nullable: boolean;
|
|
40
|
+
/** 是否有默认值。true 时以 defaultValue 文本为准(可为 '',表示 DEFAULT '') */
|
|
41
|
+
hasDefault: boolean;
|
|
42
|
+
/** 默认值文本;'NULL' 视为 DEFAULT NULL;函数/表达式按 expression 归类 */
|
|
43
|
+
defaultValue?: string | null;
|
|
44
|
+
/** 是否自增(原始意图;生效与否由后端按「必须依附主键」clamp) */
|
|
45
|
+
autoIncrement?: boolean;
|
|
46
|
+
/** 注释 */
|
|
47
|
+
comment?: string;
|
|
48
|
+
}
|
|
49
|
+
/** 目标表定义(前端表单 -> 后端) */
|
|
50
|
+
export interface TableSpec {
|
|
51
|
+
name: string;
|
|
52
|
+
comment?: string;
|
|
53
|
+
columns: ColumnSpec[];
|
|
54
|
+
/**
|
|
55
|
+
* 表级主键(复合主键按序)。主键不挂在列上——这是单归属的关键:
|
|
56
|
+
* 勾选/取消主键只改这个数组,绝不产生列级 modified 记账。
|
|
57
|
+
*/
|
|
58
|
+
primaryKey: string[];
|
|
59
|
+
}
|
|
60
|
+
/** 归一化后的列(比较用的中间形状,与来源表示无关) */
|
|
61
|
+
export interface CanonicalColumn {
|
|
62
|
+
name: string;
|
|
63
|
+
/** 大写、去空格的基础类型;自定义类型为原样 trim 的完整串 */
|
|
64
|
+
baseType: string;
|
|
65
|
+
length?: number;
|
|
66
|
+
precision?: number;
|
|
67
|
+
scale?: number;
|
|
68
|
+
nullable: boolean;
|
|
69
|
+
defaultKind: DefaultKind;
|
|
70
|
+
/** literal: 归一后的字面量文本;expression: 大写 trim 的表达式串;其余为空 */
|
|
71
|
+
defaultText: string;
|
|
72
|
+
/** 原始自增意图(不做 clamp,供"加主键时恢复自增"使用) */
|
|
73
|
+
rawAutoIncrement: boolean;
|
|
74
|
+
/** clamp 后的自增:rawAutoIncrement 且列在主键中 且 类型支持 */
|
|
75
|
+
effectiveAutoIncrement: boolean;
|
|
76
|
+
/** trim 后的注释,无注释为 '' */
|
|
77
|
+
comment: string;
|
|
78
|
+
}
|
|
79
|
+
/** 归一化后的表(diff 的两个输入) */
|
|
80
|
+
export interface CanonicalTable {
|
|
81
|
+
name: string;
|
|
82
|
+
comment: string;
|
|
83
|
+
columns: CanonicalColumn[];
|
|
84
|
+
/** 主键列名(按序) */
|
|
85
|
+
primaryKey: string[];
|
|
86
|
+
}
|
|
87
|
+
/** 列级被修改的维度(不含 name、不含主键) */
|
|
88
|
+
export type ColumnModifiedField = 'type' | 'nullable' | 'default' | 'comment' | 'autoIncrement';
|
|
89
|
+
export interface ColumnModification {
|
|
90
|
+
/** 以目标列名为准 */
|
|
91
|
+
name: string;
|
|
92
|
+
/** 恰好发生变化的维度(空数组不可能出现) */
|
|
93
|
+
fields: ColumnModifiedField[];
|
|
94
|
+
before: CanonicalColumn;
|
|
95
|
+
after: CanonicalColumn;
|
|
96
|
+
}
|
|
97
|
+
export interface ColumnRename {
|
|
98
|
+
from: string;
|
|
99
|
+
to: string;
|
|
100
|
+
/** 改名的同时定义也变了(由方言层合并为 RENAME+MODIFY / CHANGE COLUMN) */
|
|
101
|
+
definitionChanged: boolean;
|
|
102
|
+
modifiedFields: ColumnModifiedField[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 表结构差异(互斥且完备):
|
|
106
|
+
* 任一列名至多出现在 added / dropped / renamed / modified 之一;
|
|
107
|
+
* 主键变更独立于列桶;空 ChangeSet ⟺ 两表归一化后等价。
|
|
108
|
+
*/
|
|
109
|
+
export interface TableChangeSet {
|
|
110
|
+
rename: {
|
|
111
|
+
from: string;
|
|
112
|
+
to: string;
|
|
113
|
+
} | null;
|
|
114
|
+
comment: {
|
|
115
|
+
from: string;
|
|
116
|
+
to: string;
|
|
117
|
+
} | null;
|
|
118
|
+
added: CanonicalColumn[];
|
|
119
|
+
/** 被删除的原始列名(未被识别为改名的一侧) */
|
|
120
|
+
dropped: string[];
|
|
121
|
+
renamed: ColumnRename[];
|
|
122
|
+
modified: ColumnModification[];
|
|
123
|
+
/** 幸存列的相对顺序是否变化(SQLite 重建表等场景需要) */
|
|
124
|
+
orderChanged: boolean;
|
|
125
|
+
/** 目标完整列序(重建表时使用) */
|
|
126
|
+
columnOrder: string[];
|
|
127
|
+
primaryKey: {
|
|
128
|
+
from: string[];
|
|
129
|
+
to: string[];
|
|
130
|
+
} | null;
|
|
131
|
+
}
|
|
132
|
+
/** ChangeSet 是否为空(预览/保存按钮的「没有检测到任何修改」统一判据) */
|
|
133
|
+
export declare function isEmptyChangeSet(cs: TableChangeSet): boolean;
|
|
134
|
+
/** 方言 DDL 能力(capabilities 接口的返回;前端据此渲染 UI,不再硬编码方言表) */
|
|
135
|
+
export interface DdlCapability {
|
|
136
|
+
createTable: boolean;
|
|
137
|
+
alterTable: boolean;
|
|
138
|
+
dropTable: boolean;
|
|
139
|
+
renameTable: boolean;
|
|
140
|
+
alterColumn: boolean;
|
|
141
|
+
renameColumn: boolean;
|
|
142
|
+
alterPrimaryKey: boolean;
|
|
143
|
+
tableComment: boolean;
|
|
144
|
+
columnComment: boolean;
|
|
145
|
+
/** native=原生 ALTER;rebuild=需重建表(SQLite);unsupported=不支持(MongoDB) */
|
|
146
|
+
alterStrategy: 'native' | 'rebuild' | 'unsupported';
|
|
147
|
+
quoteStyle: 'backtick' | 'double' | 'bracket';
|
|
148
|
+
}
|
|
149
|
+
/** 预览结果:纯计算,不触碰数据库(建表场景无 diff,changeSet 为 null) */
|
|
150
|
+
export interface DdlPreview {
|
|
151
|
+
statements: string[];
|
|
152
|
+
preview: string;
|
|
153
|
+
changeSet: TableChangeSet | null;
|
|
154
|
+
}
|
|
155
|
+
/** 执行结果 */
|
|
156
|
+
export interface DdlExecuteResult {
|
|
157
|
+
statements: string[];
|
|
158
|
+
/** 执行时后端重新读取的实时结构与前端 baseline 不一致时拒绝执行(409) */
|
|
159
|
+
conflict?: {
|
|
160
|
+
reason: string;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=ddl-spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddl-spec.d.ts","sourceRoot":"","sources":["../../../server/model/ddl-spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DDL 契约(单一事实来源)
|
|
4
|
+
*
|
|
5
|
+
* 本文件是「前端表单 <-> 后端 schema 引擎」之间唯一的类型契约,
|
|
6
|
+
* 前端直接 import 本文件(前端 tsconfig 已包含 server 目录),禁止再复制一份。
|
|
7
|
+
*
|
|
8
|
+
* 核心不变式(由 server/core/schema 保证,测试钉死):
|
|
9
|
+
* 1. 纯函数性:ChangeSet = f(canonical(原始态), canonical(目标态)),与操作路径无关;
|
|
10
|
+
* 任何"改了又改回"的 UI 操作序列,diff 结果恒为空(幂等)。
|
|
11
|
+
* 2. 单归属:每个语义维度只出现在一个桶里——
|
|
12
|
+
* 列定义在 modified,主键在 primaryKey(列上不存在 isPrimary),改名在 renamed。
|
|
13
|
+
* 3. 归一化后比较:两侧先投影为 CanonicalTable 再 diff,
|
|
14
|
+
* 表示差异(''/null/0、大小写、空格、'varchar(255)' vs {type,length})不产生假 diff。
|
|
15
|
+
*
|
|
16
|
+
* 注意:ColumnSpec 有 hasDefault 布尔,解决 ColumnEntity.defaultValue?: any 的三态歧义
|
|
17
|
+
* (无法区分「无默认值」/「DEFAULT NULL」/「DEFAULT 0」/「DEFAULT ''」)。
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.isEmptyChangeSet = isEmptyChangeSet;
|
|
21
|
+
/** ChangeSet 是否为空(预览/保存按钮的「没有检测到任何修改」统一判据) */
|
|
22
|
+
function isEmptyChangeSet(cs) {
|
|
23
|
+
return (cs.rename === null &&
|
|
24
|
+
cs.comment === null &&
|
|
25
|
+
cs.added.length === 0 &&
|
|
26
|
+
cs.dropped.length === 0 &&
|
|
27
|
+
cs.renamed.length === 0 &&
|
|
28
|
+
cs.modified.length === 0 &&
|
|
29
|
+
!cs.orderChanged &&
|
|
30
|
+
cs.primaryKey === null);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=ddl-spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddl-spec.js","sourceRoot":"","sources":["../../../server/model/ddl-spec.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;AAqHH,4CAWC;AAZD,8CAA8C;AAC9C,SAAgB,gBAAgB,CAAC,EAAkB;IACjD,OAAO,CACL,EAAE,CAAC,MAAM,KAAK,IAAI;QAClB,EAAE,CAAC,OAAO,KAAK,IAAI;QACnB,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QACrB,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QACvB,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QACvB,EAAE,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QACxB,CAAC,EAAE,CAAC,YAAY;QAChB,EAAE,CAAC,UAAU,KAAK,IAAI,CACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DDL 契约(单一事实来源)
|
|
3
|
+
*
|
|
4
|
+
* 本文件是「前端表单 <-> 后端 schema 引擎」之间唯一的类型契约,
|
|
5
|
+
* 前端直接 import 本文件(前端 tsconfig 已包含 server 目录),禁止再复制一份。
|
|
6
|
+
*
|
|
7
|
+
* 核心不变式(由 server/core/schema 保证,测试钉死):
|
|
8
|
+
* 1. 纯函数性:ChangeSet = f(canonical(原始态), canonical(目标态)),与操作路径无关;
|
|
9
|
+
* 任何"改了又改回"的 UI 操作序列,diff 结果恒为空(幂等)。
|
|
10
|
+
* 2. 单归属:每个语义维度只出现在一个桶里——
|
|
11
|
+
* 列定义在 modified,主键在 primaryKey(列上不存在 isPrimary),改名在 renamed。
|
|
12
|
+
* 3. 归一化后比较:两侧先投影为 CanonicalTable 再 diff,
|
|
13
|
+
* 表示差异(''/null/0、大小写、空格、'varchar(255)' vs {type,length})不产生假 diff。
|
|
14
|
+
*
|
|
15
|
+
* 注意:ColumnSpec 有 hasDefault 布尔,解决 ColumnEntity.defaultValue?: any 的三态歧义
|
|
16
|
+
* (无法区分「无默认值」/「DEFAULT NULL」/「DEFAULT 0」/「DEFAULT ''」)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 默认值语义类别(归一化后的三态+表达式分类) */
|
|
20
|
+
export type DefaultKind = 'none' | 'null' | 'literal' | 'expression';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 目标列定义(前端表单 -> 后端)
|
|
24
|
+
* 前端负责把 UI 状态整理成该结构;不发送 UI 哨兵值(如 '__custom__'),
|
|
25
|
+
* 自定义类型直接放 customType。
|
|
26
|
+
*/
|
|
27
|
+
export interface ColumnSpec {
|
|
28
|
+
/** 列名(原样,不去大小写化——标识符匹配按精确名) */
|
|
29
|
+
name: string;
|
|
30
|
+
/** 基础类型名,如 VARCHAR / DECIMAL(大小写均可,后端归一为大写比较) */
|
|
31
|
+
type: string;
|
|
32
|
+
/** 完整自定义类型串,如 ENUM('a','b')、DECIMAL(20,0);非空时忽略 type/length/precision/scale */
|
|
33
|
+
customType?: string;
|
|
34
|
+
/** 字符长度(string/number 均可,''/null 归一为未设置) */
|
|
35
|
+
length?: number | string | null;
|
|
36
|
+
/** 数值精度(precision=总位数;scale=小数位数。注意与 scale 区分) */
|
|
37
|
+
precision?: number | string | null;
|
|
38
|
+
/** 小数位数(DECIMAL(10,0) 的 0 必须保留,不得因假值被丢弃) */
|
|
39
|
+
scale?: number | string | null;
|
|
40
|
+
/** 是否允许 NULL */
|
|
41
|
+
nullable: boolean;
|
|
42
|
+
/** 是否有默认值。true 时以 defaultValue 文本为准(可为 '',表示 DEFAULT '') */
|
|
43
|
+
hasDefault: boolean;
|
|
44
|
+
/** 默认值文本;'NULL' 视为 DEFAULT NULL;函数/表达式按 expression 归类 */
|
|
45
|
+
defaultValue?: string | null;
|
|
46
|
+
/** 是否自增(原始意图;生效与否由后端按「必须依附主键」clamp) */
|
|
47
|
+
autoIncrement?: boolean;
|
|
48
|
+
/** 注释 */
|
|
49
|
+
comment?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 目标表定义(前端表单 -> 后端) */
|
|
53
|
+
export interface TableSpec {
|
|
54
|
+
name: string;
|
|
55
|
+
comment?: string;
|
|
56
|
+
columns: ColumnSpec[];
|
|
57
|
+
/**
|
|
58
|
+
* 表级主键(复合主键按序)。主键不挂在列上——这是单归属的关键:
|
|
59
|
+
* 勾选/取消主键只改这个数组,绝不产生列级 modified 记账。
|
|
60
|
+
*/
|
|
61
|
+
primaryKey: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 归一化后的列(比较用的中间形状,与来源表示无关) */
|
|
65
|
+
export interface CanonicalColumn {
|
|
66
|
+
name: string;
|
|
67
|
+
/** 大写、去空格的基础类型;自定义类型为原样 trim 的完整串 */
|
|
68
|
+
baseType: string;
|
|
69
|
+
length?: number;
|
|
70
|
+
precision?: number;
|
|
71
|
+
scale?: number;
|
|
72
|
+
nullable: boolean;
|
|
73
|
+
defaultKind: DefaultKind;
|
|
74
|
+
/** literal: 归一后的字面量文本;expression: 大写 trim 的表达式串;其余为空 */
|
|
75
|
+
defaultText: string;
|
|
76
|
+
/** 原始自增意图(不做 clamp,供"加主键时恢复自增"使用) */
|
|
77
|
+
rawAutoIncrement: boolean;
|
|
78
|
+
/** clamp 后的自增:rawAutoIncrement 且列在主键中 且 类型支持 */
|
|
79
|
+
effectiveAutoIncrement: boolean;
|
|
80
|
+
/** trim 后的注释,无注释为 '' */
|
|
81
|
+
comment: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 归一化后的表(diff 的两个输入) */
|
|
85
|
+
export interface CanonicalTable {
|
|
86
|
+
name: string;
|
|
87
|
+
comment: string;
|
|
88
|
+
columns: CanonicalColumn[];
|
|
89
|
+
/** 主键列名(按序) */
|
|
90
|
+
primaryKey: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 列级被修改的维度(不含 name、不含主键) */
|
|
94
|
+
export type ColumnModifiedField = 'type' | 'nullable' | 'default' | 'comment' | 'autoIncrement';
|
|
95
|
+
|
|
96
|
+
export interface ColumnModification {
|
|
97
|
+
/** 以目标列名为准 */
|
|
98
|
+
name: string;
|
|
99
|
+
/** 恰好发生变化的维度(空数组不可能出现) */
|
|
100
|
+
fields: ColumnModifiedField[];
|
|
101
|
+
before: CanonicalColumn;
|
|
102
|
+
after: CanonicalColumn;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ColumnRename {
|
|
106
|
+
from: string;
|
|
107
|
+
to: string;
|
|
108
|
+
/** 改名的同时定义也变了(由方言层合并为 RENAME+MODIFY / CHANGE COLUMN) */
|
|
109
|
+
definitionChanged: boolean;
|
|
110
|
+
modifiedFields: ColumnModifiedField[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 表结构差异(互斥且完备):
|
|
115
|
+
* 任一列名至多出现在 added / dropped / renamed / modified 之一;
|
|
116
|
+
* 主键变更独立于列桶;空 ChangeSet ⟺ 两表归一化后等价。
|
|
117
|
+
*/
|
|
118
|
+
export interface TableChangeSet {
|
|
119
|
+
rename: { from: string; to: string } | null;
|
|
120
|
+
comment: { from: string; to: string } | null;
|
|
121
|
+
added: CanonicalColumn[];
|
|
122
|
+
/** 被删除的原始列名(未被识别为改名的一侧) */
|
|
123
|
+
dropped: string[];
|
|
124
|
+
renamed: ColumnRename[];
|
|
125
|
+
modified: ColumnModification[];
|
|
126
|
+
/** 幸存列的相对顺序是否变化(SQLite 重建表等场景需要) */
|
|
127
|
+
orderChanged: boolean;
|
|
128
|
+
/** 目标完整列序(重建表时使用) */
|
|
129
|
+
columnOrder: string[];
|
|
130
|
+
primaryKey: { from: string[]; to: string[] } | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** ChangeSet 是否为空(预览/保存按钮的「没有检测到任何修改」统一判据) */
|
|
134
|
+
export function isEmptyChangeSet(cs: TableChangeSet): boolean {
|
|
135
|
+
return (
|
|
136
|
+
cs.rename === null &&
|
|
137
|
+
cs.comment === null &&
|
|
138
|
+
cs.added.length === 0 &&
|
|
139
|
+
cs.dropped.length === 0 &&
|
|
140
|
+
cs.renamed.length === 0 &&
|
|
141
|
+
cs.modified.length === 0 &&
|
|
142
|
+
!cs.orderChanged &&
|
|
143
|
+
cs.primaryKey === null
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 方言 DDL 能力(capabilities 接口的返回;前端据此渲染 UI,不再硬编码方言表) */
|
|
148
|
+
export interface DdlCapability {
|
|
149
|
+
createTable: boolean;
|
|
150
|
+
alterTable: boolean;
|
|
151
|
+
dropTable: boolean;
|
|
152
|
+
renameTable: boolean;
|
|
153
|
+
alterColumn: boolean;
|
|
154
|
+
renameColumn: boolean;
|
|
155
|
+
alterPrimaryKey: boolean;
|
|
156
|
+
tableComment: boolean;
|
|
157
|
+
columnComment: boolean;
|
|
158
|
+
/** native=原生 ALTER;rebuild=需重建表(SQLite);unsupported=不支持(MongoDB) */
|
|
159
|
+
alterStrategy: 'native' | 'rebuild' | 'unsupported';
|
|
160
|
+
quoteStyle: 'backtick' | 'double' | 'bracket';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 预览结果:纯计算,不触碰数据库(建表场景无 diff,changeSet 为 null) */
|
|
164
|
+
export interface DdlPreview {
|
|
165
|
+
statements: string[];
|
|
166
|
+
preview: string;
|
|
167
|
+
changeSet: TableChangeSet | null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 执行结果 */
|
|
171
|
+
export interface DdlExecuteResult {
|
|
172
|
+
statements: string[];
|
|
173
|
+
/** 执行时后端重新读取的实时结构与前端 baseline 不一致时拒绝执行(409) */
|
|
174
|
+
conflict?: { reason: string };
|
|
175
|
+
}
|
|
@@ -14,6 +14,14 @@ export declare class ConnectionService {
|
|
|
14
14
|
* 活跃的数据库连接实例
|
|
15
15
|
*/
|
|
16
16
|
private activeConnections;
|
|
17
|
+
/**
|
|
18
|
+
* 正在建立中的连接(key → Promise)。
|
|
19
|
+
* 缓存里存的是已建好的 DataSource,同一 key 的并发请求在 await 期间都读不到缓存,
|
|
20
|
+
* 会各自 new DataSource().initialize(),后写的覆盖先写的——先建的那个池再无引用也
|
|
21
|
+
* 无法回收,持续累积直到服务端报 ER_CON_COUNT_ERROR(Too many connections)。
|
|
22
|
+
* 这里登记在途 Promise,让并发请求共享同一次建连。
|
|
23
|
+
*/
|
|
24
|
+
private pendingConnections;
|
|
17
25
|
constructor();
|
|
18
26
|
/**
|
|
19
27
|
* 初始化服务,创建配置目录
|
|
@@ -41,12 +49,24 @@ export declare class ConnectionService {
|
|
|
41
49
|
deleteConnection(id: string): Promise<void>;
|
|
42
50
|
/**
|
|
43
51
|
* 测试数据库连接
|
|
52
|
+
*
|
|
53
|
+
* 当请求带 id 时,优先用服务端已保存的配置补齐客户端未回传的凭据
|
|
54
|
+
* (远程代理场景下客户端不持有远程库的密码,密码只保存在服务端)。
|
|
44
55
|
*/
|
|
45
56
|
testConnection(connection: ConnectionEntity): Promise<boolean>;
|
|
46
57
|
/**
|
|
47
58
|
* 获取活跃的数据库连接
|
|
59
|
+
*
|
|
60
|
+
* 并发安全:同一 key 的并发请求共享同一次建连(pendingConnections 去重),
|
|
61
|
+
* 避免重复建连导致的连接池泄漏(MySQL Too many connections 的主因)。
|
|
48
62
|
*/
|
|
49
63
|
getActiveConnection(id: string, database?: string): Promise<DataSource>;
|
|
64
|
+
/** 建连并登记到缓存;超过容量上限时按 LRU 淘汰最久未使用的连接 */
|
|
65
|
+
private createAndCacheConnection;
|
|
66
|
+
/** 按 LRU 把缓存裁剪到上限,淘汰项真正 destroy,避免连接池残留 */
|
|
67
|
+
private trimCache;
|
|
68
|
+
/** 销毁连接但不向上抛,避免清理失败影响主流程 */
|
|
69
|
+
private destroyQuietly;
|
|
50
70
|
/**
|
|
51
71
|
* 关闭数据库连接
|
|
52
72
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.service.d.ts","sourceRoot":"","sources":["../../../server/service/connection.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"connection.service.d.ts","sourceRoot":"","sources":["../../../server/service/connection.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAU7D;;;GAGG;AACH,qBAAa,iBAAiB;IAE5B;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC;;OAEG;IACH,OAAO,CAAC,iBAAiB,CAAsC;IAE/D;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB,CAA+C;;IAMzE;;OAEG;IACG,IAAI;IAIV;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAatD;;OAEG;IACG,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAKrE;;OAEG;IACG,aAAa,CAAC,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoB5E;;OAEG;IACG,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyBjG;;OAEG;IACG,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBjD;;;;;OAKG;IACG,cAAc,CAAC,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBpE;;;;;OAKG;IACG,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAgC7E,uCAAuC;YACzB,wBAAwB;IAuBtC,2CAA2C;YAC7B,SAAS;IAUvB,4BAA4B;YACd,cAAc;IAS5B;;OAEG;IACG,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAanE;;OAEG;IACG,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAenE;;OAEG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAW1C;;OAEG;YACW,uBAAuB;IAUrC;;;OAGG;IACG,yBAAyB,CAAC,iBAAiB,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC;IAkB1F;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAsFzB;;OAEG;YACW,eAAe;IAkB7B;;OAEG;IACH,OAAO,CAAC,UAAU;CAGnB"}
|
|
@@ -38,6 +38,12 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const typeorm_1 = require("typeorm");
|
|
40
40
|
const data_dir_1 = require("../utils/data-dir");
|
|
41
|
+
/**
|
|
42
|
+
* 缓存的连接池数量上限。
|
|
43
|
+
* 每个池自身还有 connectionLimit 上限(见 getTypeORMOptions),
|
|
44
|
+
* 二者相乘即本进程对单个库的最大连接数,需留出余量避免撞上服务端 max_connections。
|
|
45
|
+
*/
|
|
46
|
+
const MAX_CACHED_CONNECTIONS = 8;
|
|
41
47
|
/**
|
|
42
48
|
* 数据库连接管理服务
|
|
43
49
|
* 负责管理数据库连接配置和连接实例
|
|
@@ -52,6 +58,14 @@ class ConnectionService {
|
|
|
52
58
|
* 活跃的数据库连接实例
|
|
53
59
|
*/
|
|
54
60
|
activeConnections = new Map();
|
|
61
|
+
/**
|
|
62
|
+
* 正在建立中的连接(key → Promise)。
|
|
63
|
+
* 缓存里存的是已建好的 DataSource,同一 key 的并发请求在 await 期间都读不到缓存,
|
|
64
|
+
* 会各自 new DataSource().initialize(),后写的覆盖先写的——先建的那个池再无引用也
|
|
65
|
+
* 无法回收,持续累积直到服务端报 ER_CON_COUNT_ERROR(Too many connections)。
|
|
66
|
+
* 这里登记在途 Promise,让并发请求共享同一次建连。
|
|
67
|
+
*/
|
|
68
|
+
pendingConnections = new Map();
|
|
55
69
|
constructor() {
|
|
56
70
|
this.configPath = (0, data_dir_1.getDataPath)('connections.json');
|
|
57
71
|
}
|
|
@@ -115,6 +129,12 @@ class ConnectionService {
|
|
|
115
129
|
if (updates.name && connections.find((conn, idx) => conn.name === updates.name && idx !== index)) {
|
|
116
130
|
throw new Error('连接名称已存在');
|
|
117
131
|
}
|
|
132
|
+
// 空密码不覆盖已保存的密码:
|
|
133
|
+
// 远程代理场景下客户端不持有远程库的密码(服务端不下发),编辑保存时密码框为空,
|
|
134
|
+
// 若直接覆盖会把远端的密码清掉。
|
|
135
|
+
if (updates.password === '' && connections[index].password) {
|
|
136
|
+
delete updates.password;
|
|
137
|
+
}
|
|
118
138
|
// @ts-ignore
|
|
119
139
|
connections[index] = { ...connections[index], ...updates, updatedAt: new Date() };
|
|
120
140
|
await this.saveConnections(connections);
|
|
@@ -139,10 +159,20 @@ class ConnectionService {
|
|
|
139
159
|
}
|
|
140
160
|
/**
|
|
141
161
|
* 测试数据库连接
|
|
162
|
+
*
|
|
163
|
+
* 当请求带 id 时,优先用服务端已保存的配置补齐客户端未回传的凭据
|
|
164
|
+
* (远程代理场景下客户端不持有远程库的密码,密码只保存在服务端)。
|
|
142
165
|
*/
|
|
143
166
|
async testConnection(connection) {
|
|
144
167
|
try {
|
|
145
|
-
|
|
168
|
+
let target = connection;
|
|
169
|
+
if (connection.id) {
|
|
170
|
+
const stored = await this.getConnectionById(connection.id);
|
|
171
|
+
if (stored) {
|
|
172
|
+
target = { ...stored, ...connection, password: connection.password || stored.password };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const tempDataSource = await this.createTypeORMDataSource(target);
|
|
146
176
|
await tempDataSource.query('SELECT 1');
|
|
147
177
|
await tempDataSource.destroy();
|
|
148
178
|
return true;
|
|
@@ -153,33 +183,42 @@ class ConnectionService {
|
|
|
153
183
|
}
|
|
154
184
|
/**
|
|
155
185
|
* 获取活跃的数据库连接
|
|
186
|
+
*
|
|
187
|
+
* 并发安全:同一 key 的并发请求共享同一次建连(pendingConnections 去重),
|
|
188
|
+
* 避免重复建连导致的连接池泄漏(MySQL Too many connections 的主因)。
|
|
156
189
|
*/
|
|
157
190
|
async getActiveConnection(id, database) {
|
|
158
191
|
const key = database ? `${id}_${database}` : id;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
return db;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
// 连接已关闭,从缓存中移除
|
|
192
|
+
// 命中缓存:可用则复用,并刷新 LRU 位置(避免正在使用的热点连接被优先淘汰)
|
|
193
|
+
const cached = this.activeConnections.get(key);
|
|
194
|
+
if (cached) {
|
|
195
|
+
if (cached.isInitialized) {
|
|
167
196
|
this.activeConnections.delete(key);
|
|
197
|
+
this.activeConnections.set(key, cached);
|
|
198
|
+
return cached;
|
|
168
199
|
}
|
|
200
|
+
// 连接已失效:必须销毁后再移除,否则它持有的连接池会泄漏
|
|
201
|
+
this.activeConnections.delete(key);
|
|
202
|
+
await this.destroyQuietly(cached, key);
|
|
169
203
|
}
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
// 并发去重:同一 key 已在建连中,直接复用同一个 Promise
|
|
205
|
+
const pending = this.pendingConnections.get(key);
|
|
206
|
+
if (pending)
|
|
207
|
+
return pending;
|
|
208
|
+
const task = this.createAndCacheConnection(key, id, database);
|
|
209
|
+
this.pendingConnections.set(key, task);
|
|
210
|
+
try {
|
|
211
|
+
return await task;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
// 仅清理自己登记的那一个(可能已被同 key 的后续任务覆盖)
|
|
215
|
+
if (this.pendingConnections.get(key) === task) {
|
|
216
|
+
this.pendingConnections.delete(key);
|
|
180
217
|
}
|
|
181
|
-
this.activeConnections.delete(oldestKey);
|
|
182
218
|
}
|
|
219
|
+
}
|
|
220
|
+
/** 建连并登记到缓存;超过容量上限时按 LRU 淘汰最久未使用的连接 */
|
|
221
|
+
async createAndCacheConnection(key, id, database) {
|
|
183
222
|
const connectionConfig = await this.getConnectionById(id);
|
|
184
223
|
if (!connectionConfig) {
|
|
185
224
|
throw new Error('连接配置不存在');
|
|
@@ -191,8 +230,34 @@ class ConnectionService {
|
|
|
191
230
|
};
|
|
192
231
|
const dataSource = await this.createTypeORMDataSource(updatedConnectionConfig);
|
|
193
232
|
this.activeConnections.set(key, dataSource);
|
|
233
|
+
// 必须「注册后再裁剪」:若在 await 建连之前检查容量,并发请求会在同一时刻
|
|
234
|
+
// 全部通过检查(此刻谁都还没 set 进缓存),缓存就会突破上限。
|
|
235
|
+
// set 之后 size 才是真实值;新连接位于 LRU 末尾,裁剪不会误伤它。
|
|
236
|
+
await this.trimCache();
|
|
194
237
|
return dataSource;
|
|
195
238
|
}
|
|
239
|
+
/** 按 LRU 把缓存裁剪到上限,淘汰项真正 destroy,避免连接池残留 */
|
|
240
|
+
async trimCache() {
|
|
241
|
+
while (this.activeConnections.size > MAX_CACHED_CONNECTIONS) {
|
|
242
|
+
const oldestKey = this.activeConnections.keys().next().value;
|
|
243
|
+
if (!oldestKey)
|
|
244
|
+
break;
|
|
245
|
+
const oldest = this.activeConnections.get(oldestKey);
|
|
246
|
+
this.activeConnections.delete(oldestKey);
|
|
247
|
+
await this.destroyQuietly(oldest, oldestKey);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** 销毁连接但不向上抛,避免清理失败影响主流程 */
|
|
251
|
+
async destroyQuietly(dataSource, label) {
|
|
252
|
+
if (!dataSource)
|
|
253
|
+
return;
|
|
254
|
+
try {
|
|
255
|
+
await dataSource.destroy();
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error(`关闭连接 ${label} 失败:`, error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
196
261
|
/**
|
|
197
262
|
* 关闭数据库连接
|
|
198
263
|
*/
|
|
@@ -278,6 +343,7 @@ class ConnectionService {
|
|
|
278
343
|
* 获取TypeORM连接配置
|
|
279
344
|
*/
|
|
280
345
|
getTypeORMOptions(connectionConfig) {
|
|
346
|
+
const userOptions = (connectionConfig.options || {});
|
|
281
347
|
const baseOptions = {
|
|
282
348
|
type: connectionConfig.type,
|
|
283
349
|
host: connectionConfig.host,
|
|
@@ -291,11 +357,17 @@ class ConnectionService {
|
|
|
291
357
|
// 全局安装时 Node 会解析到系统中全局遗留的 mysql 包,该老包不支持 MySQL 8 的
|
|
292
358
|
// caching_sha2_password,导致 ER_NOT_SUPPORTED_AUTH_MODE。显式指定 mysql2 可彻底规避。
|
|
293
359
|
connectorPackage: 'mysql2',
|
|
294
|
-
|
|
360
|
+
...userOptions,
|
|
361
|
+
// extra 必须放在 ...userOptions 之后并做合并:
|
|
362
|
+
// 原先 extra 在展开之前,connectionConfig.options.extra 会把它整体顶掉,
|
|
363
|
+
// 导致 multipleStatements 静默失效。
|
|
364
|
+
// connectionLimit 显式收敛单池上限(mysql2 默认 10),叠加最多 MAX_CACHED_CONNECTIONS
|
|
365
|
+
// 个池后仍远小于常见的 max_connections(151),避免 Too many connections。
|
|
295
366
|
extra: {
|
|
296
|
-
multipleStatements: true
|
|
297
|
-
|
|
298
|
-
|
|
367
|
+
multipleStatements: true,
|
|
368
|
+
connectionLimit: 5,
|
|
369
|
+
...(userOptions.extra || {})
|
|
370
|
+
}
|
|
299
371
|
};
|
|
300
372
|
// 根据数据库类型调整配置
|
|
301
373
|
switch (connectionConfig.type.toLowerCase()) {
|
|
@@ -316,7 +388,9 @@ class ConnectionService {
|
|
|
316
388
|
return {
|
|
317
389
|
...baseOptions,
|
|
318
390
|
type: 'postgres',
|
|
319
|
-
ssl: connectionConfig.options?.ssl || false
|
|
391
|
+
ssl: connectionConfig.options?.ssl || false,
|
|
392
|
+
// pg 池上限字段是 max(不是 mysql2 的 connectionLimit)
|
|
393
|
+
extra: { ...(baseOptions.extra || {}), max: 5 }
|
|
320
394
|
};
|
|
321
395
|
case 'oracle':
|
|
322
396
|
return {
|