midway-fatcms 0.0.5 → 0.0.7
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/.qoder/skills/midway-fatcms-crud/SKILL.md +375 -0
- package/.qoder/skills/midway-fatcms-crud/examples.md +990 -0
- package/.qoder/skills/midway-fatcms-crud/reference.md +568 -0
- package/README.md +377 -134
- package/dist/controller/manage/CrudStandardDesignApi.d.ts +0 -2
- package/dist/controller/manage/CrudStandardDesignApi.js +11 -85
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/libs/crud-pro/CrudPro.d.ts +9 -1
- package/dist/libs/crud-pro/CrudPro.js +15 -0
- package/dist/libs/crud-pro/README.md +809 -0
- package/dist/libs/crud-pro/README_FUNC.md +193 -0
- package/dist/libs/crud-pro/exceptions.d.ts +2 -0
- package/dist/libs/crud-pro/exceptions.js +2 -0
- package/dist/libs/crud-pro/interfaces.d.ts +34 -1
- package/dist/libs/crud-pro/models/ExecuteContext.d.ts +3 -3
- package/dist/libs/crud-pro/models/ExecuteContext.js +2 -0
- package/dist/libs/crud-pro/models/RequestModel.d.ts +6 -2
- package/dist/libs/crud-pro/models/RequestModel.js +20 -53
- package/dist/libs/crud-pro/models/ResModel.d.ts +6 -4
- package/dist/libs/crud-pro/models/ServiceHub.d.ts +1 -0
- package/dist/libs/crud-pro/models/keys.d.ts +6 -1
- package/dist/libs/crud-pro/models/keys.js +5 -0
- package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.d.ts +52 -0
- package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.js +158 -0
- package/dist/libs/crud-pro/services/CrudProExecuteSqlService.js +20 -1
- package/dist/libs/crud-pro/services/CrudProFieldValidateService.d.ts +7 -0
- package/dist/libs/crud-pro/services/CrudProFieldValidateService.js +32 -0
- package/dist/libs/crud-pro/services/CrudProGenSqlService.d.ts +13 -0
- package/dist/libs/crud-pro/services/CrudProGenSqlService.js +44 -7
- package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.d.ts +43 -0
- package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.js +132 -1
- package/dist/libs/crud-pro/services/CrudProTableMetaService.d.ts +15 -1
- package/dist/libs/crud-pro/services/CrudProTableMetaService.js +107 -0
- package/dist/libs/crud-pro/services/CurdProServiceHub.d.ts +5 -1
- package/dist/libs/crud-pro/services/CurdProServiceHub.js +11 -0
- package/dist/libs/crud-pro/utils/DateTimeUtils.d.ts +1 -0
- package/dist/libs/crud-pro/utils/DateTimeUtils.js +3 -0
- package/dist/libs/crud-pro/utils/MixinUtils.d.ts +32 -0
- package/dist/libs/crud-pro/utils/MixinUtils.js +85 -1
- package/dist/libs/crud-pro/utils/OrderByUtils.d.ts +70 -0
- package/dist/libs/crud-pro/utils/OrderByUtils.js +158 -0
- package/dist/libs/crud-pro/utils/ValidateUtils.js +1 -1
- package/dist/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
- package/dist/libs/crud-sharding/ShardingConfig.d.ts +218 -0
- package/dist/libs/crud-sharding/ShardingConfig.js +32 -0
- package/dist/libs/crud-sharding/ShardingCountCache.d.ts +69 -0
- package/dist/libs/crud-sharding/ShardingCountCache.js +160 -0
- package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +363 -0
- package/dist/libs/crud-sharding/ShardingCrudPro.js +675 -0
- package/dist/libs/crud-sharding/ShardingMerger.d.ts +130 -0
- package/dist/libs/crud-sharding/ShardingMerger.js +282 -0
- package/dist/libs/crud-sharding/ShardingRouter.d.ts +69 -0
- package/dist/libs/crud-sharding/ShardingRouter.js +377 -0
- package/dist/libs/crud-sharding/ShardingTableCreator.d.ts +146 -0
- package/dist/libs/crud-sharding/ShardingTableCreator.js +805 -0
- package/dist/libs/crud-sharding/ShardingUtils.d.ts +38 -0
- package/dist/libs/crud-sharding/ShardingUtils.js +77 -0
- package/dist/libs/crud-sharding/index.d.ts +45 -0
- package/dist/libs/crud-sharding/index.js +55 -0
- package/dist/models/StandardColumns.d.ts +71 -0
- package/dist/models/StandardColumns.js +28 -0
- package/dist/service/SysAppService.js +2 -2
- package/dist/service/SysConfigService.js +1 -1
- package/dist/service/SysDictDataService.js +2 -2
- package/dist/service/SysMenuService.js +1 -1
- package/dist/service/UserAccountService.d.ts +1 -1
- package/dist/service/crudstd/CrudStdService.d.ts +0 -1
- package/dist/service/crudstd/CrudStdService.js +0 -27
- package/dist/service/curd/CrudProQuick.d.ts +134 -4
- package/dist/service/curd/CrudProQuick.js +155 -3
- package/dist/service/curd/CurdMixService.d.ts +2 -1
- package/dist/service/curd/CurdMixService.js +5 -1
- package/dist/service/curd/CurdProService.d.ts +44 -2
- package/dist/service/curd/CurdProService.js +53 -1
- package/dist/service/curd/README.md +1100 -0
- package/dist/service/curd/fixSoftDelete.d.ts +14 -0
- package/dist/service/curd/fixSoftDelete.js +29 -11
- package/dist/service/flow/FlowConfigService.js +1 -1
- package/dist/service/flow/FlowInstanceCrudService.js +1 -1
- package/package.json +4 -1
- package/src/controller/gateway/AsyncTaskController.ts +1 -1
- package/src/controller/manage/CrudStandardDesignApi.ts +16 -100
- package/src/index.ts +3 -0
- package/src/libs/crud-pro/CrudPro.ts +19 -1
- package/src/libs/crud-pro/README.md +809 -0
- package/src/libs/crud-pro/README_FUNC.md +193 -0
- package/src/libs/crud-pro/exceptions.ts +2 -0
- package/src/libs/crud-pro/interfaces.ts +38 -1
- package/src/libs/crud-pro/models/ExecuteContext.ts +6 -3
- package/src/libs/crud-pro/models/RequestModel.ts +23 -65
- package/src/libs/crud-pro/models/ResModel.ts +10 -4
- package/src/libs/crud-pro/models/ServiceHub.ts +2 -0
- package/src/libs/crud-pro/models/keys.ts +5 -0
- package/src/libs/crud-pro/services/CrudProDataTypeConvertService.ts +171 -0
- package/src/libs/crud-pro/services/CrudProExecuteSqlService.ts +24 -1
- package/src/libs/crud-pro/services/CrudProFieldValidateService.ts +53 -1
- package/src/libs/crud-pro/services/CrudProGenSqlService.ts +51 -7
- package/src/libs/crud-pro/services/CrudProOriginToExecuteSql.ts +159 -2
- package/src/libs/crud-pro/services/CrudProTableMetaService.ts +139 -1
- package/src/libs/crud-pro/services/CurdProServiceHub.ts +16 -1
- package/src/libs/crud-pro/utils/DateTimeUtils.ts +3 -0
- package/src/libs/crud-pro/utils/MixinUtils.ts +97 -1
- package/src/libs/crud-pro/utils/OrderByUtils.ts +169 -0
- package/src/libs/crud-pro/utils/ValidateUtils.ts +1 -1
- package/src/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
- package/src/libs/crud-sharding/ShardingConfig.ts +240 -0
- package/src/libs/crud-sharding/ShardingCountCache.ts +200 -0
- package/src/libs/crud-sharding/ShardingCrudPro.ts +835 -0
- package/src/libs/crud-sharding/ShardingMerger.ts +384 -0
- package/src/libs/crud-sharding/ShardingRouter.ts +512 -0
- package/src/libs/crud-sharding/ShardingTableCreator.ts +1007 -0
- package/src/libs/crud-sharding/ShardingUtils.ts +84 -0
- package/src/libs/crud-sharding/index.ts +64 -0
- package/src/models/StandardColumns.ts +76 -0
- package/src/service/FileCenterService.ts +1 -1
- package/src/service/SysAppService.ts +2 -2
- package/src/service/SysConfigService.ts +1 -1
- package/src/service/SysDictDataService.ts +2 -2
- package/src/service/SysMenuService.ts +2 -2
- package/src/service/WorkbenchService.ts +1 -1
- package/src/service/anyapi/AnyApiService.ts +1 -1
- package/src/service/asyncTask/AsyncTaskRunnerService.ts +1 -1
- package/src/service/crudstd/CrudStdService.ts +0 -32
- package/src/service/curd/CrudProQuick.ts +164 -5
- package/src/service/curd/CurdMixService.ts +7 -2
- package/src/service/curd/CurdProService.ts +62 -3
- package/src/service/curd/README.md +1100 -0
- package/src/service/curd/fixCfgModel.ts +1 -2
- package/src/service/curd/fixSoftDelete.ts +38 -16
- package/src/service/flow/FlowConfigService.ts +1 -1
- package/src/service/flow/FlowInstanceCrudService.ts +1 -1
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
import { CrudPro } from '@/libs/crud-pro/CrudPro';
|
|
2
|
+
import { ITableColumn } from '@/libs/crud-pro/interfaces';
|
|
3
|
+
import { SqlDbType } from '@/libs/crud-pro/models/keys';
|
|
4
|
+
import { IShardingConfig, IShardingTableCreateOptions, IShardingTableCreateResult } from './ShardingConfig';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 扩展列信息(在 ITableColumn 基础上增加分表建表所需的内部标记)
|
|
8
|
+
*/
|
|
9
|
+
interface IShardingTableColumn extends ITableColumn {
|
|
10
|
+
/** 是否自增列(由各数据库加载器从 EXTRA/identity 中识别) */
|
|
11
|
+
isAutoIncrement?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 分表自动创建器
|
|
16
|
+
*
|
|
17
|
+
* 根据基表结构自动创建分表,支持 MySQL、PostgreSQL、SQL Server。
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const creator = new ShardingTableCreator(crudPro, config);
|
|
21
|
+
* await creator.createTableIfNeeded('t_order_202403', {
|
|
22
|
+
* sqlDatabase: 'mydb',
|
|
23
|
+
* sqlDbType: SqlDbType.mysql,
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
export class ShardingTableCreator {
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly crudPro: CrudPro,
|
|
29
|
+
private readonly config: IShardingConfig
|
|
30
|
+
) {
|
|
31
|
+
this.validateIdentifier(this.config.baseTable, 'baseTable');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 校验表名格式,防止 SQL 注入
|
|
36
|
+
* 只允许合法的数据库标识符:以字母或下划线开头,后续只能包含字母数字下划线
|
|
37
|
+
*/
|
|
38
|
+
private validateIdentifier(tableName: string, fieldName: string): void {
|
|
39
|
+
const validIdentifierPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
40
|
+
|
|
41
|
+
if (!tableName) {
|
|
42
|
+
throw new Error(`[ShardingTableCreator] ${fieldName} 不能为空`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!validIdentifierPattern.test(tableName)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`[ShardingTableCreator] ${fieldName} 格式不合法: "${tableName}"。` +
|
|
48
|
+
`只允许以字母或下划线开头,后续包含字母、数字、下划线的标识符`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 创建分表(如果不存在)
|
|
55
|
+
*
|
|
56
|
+
* @param tableName 目标分表名
|
|
57
|
+
* @param baseCfg 基础配置(sqlDatabase, sqlDbType)
|
|
58
|
+
* @param options 创建选项
|
|
59
|
+
* @returns 创建结果
|
|
60
|
+
*/
|
|
61
|
+
public async createTableIfNeeded(
|
|
62
|
+
tableName: string,
|
|
63
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType },
|
|
64
|
+
options: IShardingTableCreateOptions = {}
|
|
65
|
+
): Promise<IShardingTableCreateResult> {
|
|
66
|
+
// 校验分表名格式,防止 SQL 注入
|
|
67
|
+
this.validateIdentifier(tableName, 'tableName');
|
|
68
|
+
|
|
69
|
+
const { copyIndexes = false } = options;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// 1. 获取基表元信息
|
|
73
|
+
const tableMeta = await this.getBaseTableMeta(baseCfg);
|
|
74
|
+
if (!tableMeta || !tableMeta.columnDetails || tableMeta.columnDetails.length === 0) {
|
|
75
|
+
throw new Error(`[ShardingTableCreator] 无法获取基表 ${this.config.baseTable} 的结构信息`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. 生成建表 SQL
|
|
79
|
+
const createSql = this.generateCreateTableSql(
|
|
80
|
+
tableName,
|
|
81
|
+
tableMeta.columnDetails,
|
|
82
|
+
baseCfg.sqlDbType,
|
|
83
|
+
options
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 3. 执行建表
|
|
87
|
+
await this.executeCreateSql(baseCfg, createSql);
|
|
88
|
+
|
|
89
|
+
// 4. 可选:复制索引
|
|
90
|
+
if (copyIndexes) {
|
|
91
|
+
await this.copyIndexes(tableName, baseCfg);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { success: true, tableName, createSql };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
tableName,
|
|
99
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 获取基表列信息
|
|
106
|
+
*/
|
|
107
|
+
private async getBaseTableMeta(
|
|
108
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
109
|
+
): Promise<{ columnDetails: ITableColumn[] } | null> {
|
|
110
|
+
try {
|
|
111
|
+
const columns = await this.loadTableColumns(baseCfg);
|
|
112
|
+
if (!columns || columns.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return { columnDetails: columns };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('[ShardingTableCreator] 获取基表元信息失败:', error);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 根据数据库类型加载表列信息
|
|
124
|
+
*/
|
|
125
|
+
private async loadTableColumns(
|
|
126
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
127
|
+
): Promise<ITableColumn[]> {
|
|
128
|
+
const { sqlDbType } = baseCfg;
|
|
129
|
+
|
|
130
|
+
if (sqlDbType === SqlDbType.mysql) {
|
|
131
|
+
return this.loadMySQLColumns(baseCfg);
|
|
132
|
+
} else if (sqlDbType === SqlDbType.postgres) {
|
|
133
|
+
return this.loadPostgreSQLColumns(baseCfg);
|
|
134
|
+
} else if (sqlDbType === SqlDbType.sqlserver) {
|
|
135
|
+
return this.loadSQLServerColumns(baseCfg);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw new Error(`[ShardingTableCreator] 不支持的数据库类型: ${sqlDbType}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 加载 MySQL 表列信息
|
|
143
|
+
*/
|
|
144
|
+
private async loadMySQLColumns(
|
|
145
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
146
|
+
): Promise<ITableColumn[]> {
|
|
147
|
+
const sql = `
|
|
148
|
+
SELECT
|
|
149
|
+
COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY,
|
|
150
|
+
COLUMN_DEFAULT, COLUMN_COMMENT, EXTRA
|
|
151
|
+
FROM information_schema.COLUMNS
|
|
152
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${this.config.baseTable}'
|
|
153
|
+
ORDER BY ORDINAL_POSITION
|
|
154
|
+
`.trim();
|
|
155
|
+
|
|
156
|
+
const result = await this.crudPro.executeSQL({
|
|
157
|
+
isNativeSQL: true,
|
|
158
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
159
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
160
|
+
executeSql: sql,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const rows = result?.rows || [];
|
|
164
|
+
return rows.map((row: any) => ({
|
|
165
|
+
name: row.COLUMN_NAME,
|
|
166
|
+
type: row.COLUMN_TYPE,
|
|
167
|
+
isNullable: row.IS_NULLABLE === 'YES',
|
|
168
|
+
isPrimaryKey: row.COLUMN_KEY === 'PRI',
|
|
169
|
+
defaultValue: row.COLUMN_DEFAULT,
|
|
170
|
+
comment: row.COLUMN_COMMENT || undefined,
|
|
171
|
+
isAutoIncrement: row.EXTRA?.includes('auto_increment') || false,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 加载 PostgreSQL 表列信息
|
|
177
|
+
*/
|
|
178
|
+
private async loadPostgreSQLColumns(
|
|
179
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
180
|
+
): Promise<ITableColumn[]> {
|
|
181
|
+
const schema = 'public';
|
|
182
|
+
const sql = `
|
|
183
|
+
SELECT
|
|
184
|
+
column_name,
|
|
185
|
+
data_type,
|
|
186
|
+
is_nullable,
|
|
187
|
+
column_default,
|
|
188
|
+
character_maximum_length
|
|
189
|
+
FROM information_schema.columns
|
|
190
|
+
WHERE table_schema = '${schema}' AND table_name = '${this.config.baseTable}'
|
|
191
|
+
ORDER BY ordinal_position
|
|
192
|
+
`.trim();
|
|
193
|
+
|
|
194
|
+
const result = await this.crudPro.executeSQL({
|
|
195
|
+
isNativeSQL: true,
|
|
196
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
197
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
198
|
+
executeSql: sql,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const rows = result?.rows || [];
|
|
202
|
+
|
|
203
|
+
// 获取主键列
|
|
204
|
+
const pkColumns = await this.getPostgreSQLPrimaryKeys(baseCfg, schema);
|
|
205
|
+
|
|
206
|
+
return rows.map((row: any) => {
|
|
207
|
+
const { value, isSerial } = this.normalizePostgreSQLDefaultValue(row.column_default);
|
|
208
|
+
return {
|
|
209
|
+
name: row.column_name,
|
|
210
|
+
type: row.data_type,
|
|
211
|
+
isNullable: row.is_nullable === 'YES',
|
|
212
|
+
isPrimaryKey: pkColumns.includes(row.column_name),
|
|
213
|
+
defaultValue: value,
|
|
214
|
+
maxLength: row.character_maximum_length,
|
|
215
|
+
isAutoIncrement: isSerial,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 获取 PostgreSQL 表主键列
|
|
222
|
+
*/
|
|
223
|
+
private async getPostgreSQLPrimaryKeys(
|
|
224
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType },
|
|
225
|
+
schema: string
|
|
226
|
+
): Promise<string[]> {
|
|
227
|
+
const sql = `
|
|
228
|
+
SELECT a.attname
|
|
229
|
+
FROM pg_index i
|
|
230
|
+
JOIN pg_class c ON i.indrelid = c.oid
|
|
231
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
232
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
|
|
233
|
+
WHERE n.nspname = '${schema}'
|
|
234
|
+
AND c.relname = '${this.config.baseTable}'
|
|
235
|
+
AND i.indisprimary
|
|
236
|
+
`.trim();
|
|
237
|
+
|
|
238
|
+
const result = await this.crudPro.executeSQL({
|
|
239
|
+
isNativeSQL: true,
|
|
240
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
241
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
242
|
+
executeSql: sql,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return (result?.rows || []).map((row: any) => row.attname);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 加载 SQL Server 表列信息
|
|
250
|
+
*/
|
|
251
|
+
private async loadSQLServerColumns(
|
|
252
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
253
|
+
): Promise<ITableColumn[]> {
|
|
254
|
+
const sql = `
|
|
255
|
+
SELECT
|
|
256
|
+
c.name AS column_name,
|
|
257
|
+
t.name AS data_type,
|
|
258
|
+
c.max_length,
|
|
259
|
+
c.is_nullable,
|
|
260
|
+
c.is_identity,
|
|
261
|
+
dc.definition AS default_value
|
|
262
|
+
FROM sys.columns c
|
|
263
|
+
JOIN sys.types t ON c.user_type_id = t.user_type_id
|
|
264
|
+
LEFT JOIN sys.default_constraints dc ON dc.parent_column_id = c.column_id AND dc.parent_object_id = c.object_id
|
|
265
|
+
WHERE c.object_id = OBJECT_ID('${this.config.baseTable}')
|
|
266
|
+
ORDER BY c.column_id
|
|
267
|
+
`.trim();
|
|
268
|
+
|
|
269
|
+
const result = await this.crudPro.executeSQL({
|
|
270
|
+
isNativeSQL: true,
|
|
271
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
272
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
273
|
+
executeSql: sql,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const rows = result?.rows || [];
|
|
277
|
+
|
|
278
|
+
// 获取主键列
|
|
279
|
+
const pkColumns = await this.getSQLServerPrimaryKeys(baseCfg);
|
|
280
|
+
|
|
281
|
+
return rows.map((row: any) => {
|
|
282
|
+
let type = row.data_type;
|
|
283
|
+
if (row.max_length > 0 && ['varchar', 'nvarchar', 'char', 'nchar'].includes(row.data_type.toLowerCase())) {
|
|
284
|
+
type = `${row.data_type}(${row.max_length})`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
name: row.column_name,
|
|
289
|
+
type,
|
|
290
|
+
isNullable: row.is_nullable,
|
|
291
|
+
isPrimaryKey: pkColumns.includes(row.column_name),
|
|
292
|
+
defaultValue: this.normalizeSQLServerDefaultValue(row.default_value),
|
|
293
|
+
isAutoIncrement: !!row.is_identity,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 获取 SQL Server 表主键列
|
|
300
|
+
*/
|
|
301
|
+
private async getSQLServerPrimaryKeys(
|
|
302
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
303
|
+
): Promise<string[]> {
|
|
304
|
+
const sql = `
|
|
305
|
+
SELECT c.name
|
|
306
|
+
FROM sys.indexes i
|
|
307
|
+
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
308
|
+
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
309
|
+
WHERE i.object_id = OBJECT_ID('${this.config.baseTable}')
|
|
310
|
+
AND i.is_primary_key = 1
|
|
311
|
+
`.trim();
|
|
312
|
+
|
|
313
|
+
const result = await this.crudPro.executeSQL({
|
|
314
|
+
isNativeSQL: true,
|
|
315
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
316
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
317
|
+
executeSql: sql,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return (result?.rows || []).map((row: any) => row.name);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 生成建表 SQL
|
|
325
|
+
*/
|
|
326
|
+
private generateCreateTableSql(
|
|
327
|
+
tableName: string,
|
|
328
|
+
columns: ITableColumn[],
|
|
329
|
+
sqlDbType: SqlDbType,
|
|
330
|
+
options: IShardingTableCreateOptions
|
|
331
|
+
): string {
|
|
332
|
+
switch (sqlDbType) {
|
|
333
|
+
case SqlDbType.mysql:
|
|
334
|
+
return this.generateMySQLCreateSql(tableName, columns, options);
|
|
335
|
+
case SqlDbType.postgres:
|
|
336
|
+
return this.generatePostgreSQLCreateSql(tableName, columns, options);
|
|
337
|
+
case SqlDbType.sqlserver:
|
|
338
|
+
return this.generateSQLServerCreateSql(tableName, columns, options);
|
|
339
|
+
default:
|
|
340
|
+
throw new Error(`[ShardingTableCreator] 不支持的数据库类型: ${sqlDbType}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============ MySQL ============
|
|
345
|
+
|
|
346
|
+
private generateMySQLCreateSql(
|
|
347
|
+
tableName: string,
|
|
348
|
+
columns: ITableColumn[],
|
|
349
|
+
options: IShardingTableCreateOptions
|
|
350
|
+
): string {
|
|
351
|
+
const columnDefs: string[] = [];
|
|
352
|
+
|
|
353
|
+
// 列定义
|
|
354
|
+
for (const col of columns) {
|
|
355
|
+
columnDefs.push(this.formatMySQLColumn(col));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 主键
|
|
359
|
+
const pkColumns = columns.filter(c => c.isPrimaryKey).map(c => `\`${c.name}\``);
|
|
360
|
+
if (pkColumns.length > 0) {
|
|
361
|
+
columnDefs.push(`PRIMARY KEY (${pkColumns.join(', ')})`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 表选项
|
|
365
|
+
const tableOptions = options.tableOptions || 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
|
|
366
|
+
|
|
367
|
+
return `CREATE TABLE IF NOT EXISTS \`${tableName}\` (\n ${columnDefs.join(',\n ')}\n) ${tableOptions}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private formatMySQLColumn(col: ITableColumn): string {
|
|
371
|
+
const parts: string[] = [`\`${col.name}\``];
|
|
372
|
+
|
|
373
|
+
// 类型
|
|
374
|
+
parts.push(col.type);
|
|
375
|
+
|
|
376
|
+
// NOT NULL
|
|
377
|
+
if (!col.isNullable) {
|
|
378
|
+
parts.push('NOT NULL');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// AUTO_INCREMENT
|
|
382
|
+
const isAutoIncrement = !!(col as IShardingTableColumn).isAutoIncrement;
|
|
383
|
+
if (isAutoIncrement) {
|
|
384
|
+
parts.push('AUTO_INCREMENT');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 默认值(AUTO_INCREMENT 列不设默认值)
|
|
388
|
+
if (!isAutoIncrement) {
|
|
389
|
+
if (col.defaultValue !== null && col.defaultValue !== undefined) {
|
|
390
|
+
parts.push(`DEFAULT ${this.formatDefaultValue(col.defaultValue, col.type)}`);
|
|
391
|
+
} else if (col.isNullable) {
|
|
392
|
+
parts.push('DEFAULT NULL');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 注释
|
|
397
|
+
if (col.comment) {
|
|
398
|
+
parts.push(`COMMENT '${this.escapeString(col.comment)}'`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return parts.join(' ');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============ PostgreSQL ============
|
|
405
|
+
|
|
406
|
+
private generatePostgreSQLCreateSql(
|
|
407
|
+
tableName: string,
|
|
408
|
+
columns: ITableColumn[],
|
|
409
|
+
options: IShardingTableCreateOptions
|
|
410
|
+
): string {
|
|
411
|
+
const columnDefs: string[] = [];
|
|
412
|
+
|
|
413
|
+
// 列定义
|
|
414
|
+
for (const col of columns) {
|
|
415
|
+
columnDefs.push(this.formatPostgreSQLColumn(col));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 主键
|
|
419
|
+
const pkColumns = columns.filter(c => c.isPrimaryKey).map(c => `"${c.name}"`);
|
|
420
|
+
if (pkColumns.length > 0) {
|
|
421
|
+
columnDefs.push(`PRIMARY KEY (${pkColumns.join(', ')})`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 表选项(PostgreSQL 表选项通过 WITH 子句)
|
|
425
|
+
const tableOptions = options.tableOptions ? ` WITH (${options.tableOptions})` : '';
|
|
426
|
+
|
|
427
|
+
return `CREATE TABLE IF NOT EXISTS "${tableName}" (\n ${columnDefs.join(',\n ')}\n)${tableOptions}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private formatPostgreSQLColumn(col: ITableColumn): string {
|
|
431
|
+
const parts: string[] = [`"${col.name}"`];
|
|
432
|
+
|
|
433
|
+
// 类型映射
|
|
434
|
+
const pgType = this.mapToPostgreSQLType(col.type);
|
|
435
|
+
parts.push(pgType);
|
|
436
|
+
|
|
437
|
+
// NOT NULL
|
|
438
|
+
if (!col.isNullable) {
|
|
439
|
+
parts.push('NOT NULL');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 默认值
|
|
443
|
+
if (col.defaultValue !== null && col.defaultValue !== undefined) {
|
|
444
|
+
parts.push(`DEFAULT ${this.formatDefaultValue(col.defaultValue, col.type)}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return parts.join(' ');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private mapToPostgreSQLType(mysqlType: string): string {
|
|
451
|
+
// MySQL -> PostgreSQL 类型映射
|
|
452
|
+
const typeMap: Record<string, string> = {
|
|
453
|
+
'int': 'INTEGER',
|
|
454
|
+
'bigint': 'BIGINT',
|
|
455
|
+
'smallint': 'SMALLINT',
|
|
456
|
+
'tinyint': 'SMALLINT',
|
|
457
|
+
'varchar': 'VARCHAR',
|
|
458
|
+
'char': 'CHAR',
|
|
459
|
+
'text': 'TEXT',
|
|
460
|
+
'longtext': 'TEXT',
|
|
461
|
+
'mediumtext': 'TEXT',
|
|
462
|
+
'datetime': 'TIMESTAMP',
|
|
463
|
+
'timestamp': 'TIMESTAMP',
|
|
464
|
+
'date': 'DATE',
|
|
465
|
+
'time': 'TIME',
|
|
466
|
+
'decimal': 'NUMERIC',
|
|
467
|
+
'double': 'DOUBLE PRECISION',
|
|
468
|
+
'float': 'REAL',
|
|
469
|
+
'blob': 'BYTEA',
|
|
470
|
+
'longblob': 'BYTEA',
|
|
471
|
+
'mediumblob': 'BYTEA',
|
|
472
|
+
'json': 'JSONB',
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// 提取基础类型(去掉长度等参数)
|
|
476
|
+
const baseType = mysqlType.toLowerCase().split('(')[0].trim();
|
|
477
|
+
const mapped = typeMap[baseType] || mysqlType.toUpperCase();
|
|
478
|
+
|
|
479
|
+
// 保留长度参数
|
|
480
|
+
const lengthMatch = mysqlType.match(/\((\d+)(,(\d+))?\)/);
|
|
481
|
+
if (lengthMatch && (baseType === 'varchar' || baseType === 'char' || baseType === 'decimal')) {
|
|
482
|
+
return mapped + '(' + lengthMatch[1] + (lengthMatch[3] ? ',' + lengthMatch[3] : '') + ')';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return mapped;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ============ SQL Server ============
|
|
489
|
+
|
|
490
|
+
private generateSQLServerCreateSql(
|
|
491
|
+
tableName: string,
|
|
492
|
+
columns: ITableColumn[],
|
|
493
|
+
options: IShardingTableCreateOptions
|
|
494
|
+
): string {
|
|
495
|
+
const columnDefs: string[] = [];
|
|
496
|
+
|
|
497
|
+
// 列定义
|
|
498
|
+
for (const col of columns) {
|
|
499
|
+
columnDefs.push(this.formatSQLServerColumn(col));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 主键
|
|
503
|
+
const pkColumns = columns.filter(c => c.isPrimaryKey).map(c => `[${c.name}]`);
|
|
504
|
+
if (pkColumns.length > 0) {
|
|
505
|
+
columnDefs.push(`PRIMARY KEY (${pkColumns.join(', ')})`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 表选项
|
|
509
|
+
const tableOptions = options.tableOptions ? ` ${options.tableOptions}` : '';
|
|
510
|
+
|
|
511
|
+
return `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = '${tableName}')\n` +
|
|
512
|
+
`CREATE TABLE [${tableName}] (\n ${columnDefs.join(',\n ')}\n)${tableOptions}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private formatSQLServerColumn(col: ITableColumn): string {
|
|
516
|
+
const parts: string[] = [`[${col.name}]`];
|
|
517
|
+
|
|
518
|
+
// 类型映射
|
|
519
|
+
const sqlServerType = this.mapToSQLServerType(col.type);
|
|
520
|
+
parts.push(sqlServerType);
|
|
521
|
+
|
|
522
|
+
// NOT NULL
|
|
523
|
+
if (!col.isNullable) {
|
|
524
|
+
parts.push('NOT NULL');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 默认值(IDENTITY 列不设默认值)
|
|
528
|
+
const isAutoIncrement = !!(col as IShardingTableColumn).isAutoIncrement;
|
|
529
|
+
if (!isAutoIncrement && col.defaultValue !== null && col.defaultValue !== undefined) {
|
|
530
|
+
parts.push(`DEFAULT ${this.formatDefaultValue(col.defaultValue, col.type)}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 标识列(自增)
|
|
534
|
+
if (isAutoIncrement) {
|
|
535
|
+
parts.push('IDENTITY(1,1)');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return parts.join(' ');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private mapToSQLServerType(mysqlType: string): string {
|
|
542
|
+
const typeMap: Record<string, string> = {
|
|
543
|
+
'int': 'INT',
|
|
544
|
+
'bigint': 'BIGINT',
|
|
545
|
+
'smallint': 'SMALLINT',
|
|
546
|
+
'tinyint': 'TINYINT',
|
|
547
|
+
'varchar': 'NVARCHAR',
|
|
548
|
+
'char': 'NCHAR',
|
|
549
|
+
'text': 'NVARCHAR(MAX)',
|
|
550
|
+
'longtext': 'NVARCHAR(MAX)',
|
|
551
|
+
'datetime': 'DATETIME',
|
|
552
|
+
'timestamp': 'DATETIME2',
|
|
553
|
+
'date': 'DATE',
|
|
554
|
+
'time': 'TIME',
|
|
555
|
+
'decimal': 'DECIMAL',
|
|
556
|
+
'double': 'FLOAT',
|
|
557
|
+
'float': 'FLOAT',
|
|
558
|
+
'blob': 'VARBINARY(MAX)',
|
|
559
|
+
'json': 'NVARCHAR(MAX)',
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const baseType = mysqlType.toLowerCase().split('(')[0].trim();
|
|
563
|
+
const mapped = typeMap[baseType] || mysqlType.toUpperCase();
|
|
564
|
+
|
|
565
|
+
// 保留长度参数
|
|
566
|
+
const lengthMatch = mysqlType.match(/\((\d+)(,(\d+))?\)/);
|
|
567
|
+
if (lengthMatch && (baseType === 'varchar' || baseType === 'char' || baseType === 'decimal')) {
|
|
568
|
+
const len = parseInt(lengthMatch[1]);
|
|
569
|
+
// SQL Server NVARCHAR 最大 4000
|
|
570
|
+
if (baseType === 'varchar' && len > 4000) {
|
|
571
|
+
return 'NVARCHAR(MAX)';
|
|
572
|
+
}
|
|
573
|
+
return mapped + '(' + len + (lengthMatch[3] ? ',' + lengthMatch[3] : '') + ')';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return mapped;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ============ 系统目录默认值清洗 ============
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* 清理 SQL Server 系统目录中的默认值表达式
|
|
583
|
+
*
|
|
584
|
+
* sys.default_constraints.definition 返回格式如 ((1)), ('abc'), (getdate())
|
|
585
|
+
* 需要去除最外层括号,还原为可用的默认值
|
|
586
|
+
*/
|
|
587
|
+
private normalizeSQLServerDefaultValue(raw: string | null): any {
|
|
588
|
+
if (!raw) return undefined;
|
|
589
|
+
|
|
590
|
+
let val = raw.trim();
|
|
591
|
+
|
|
592
|
+
// 去除最外层成对括号(可能多层,如 ((1)) )
|
|
593
|
+
while (val.startsWith('(') && val.endsWith(')')) {
|
|
594
|
+
let depth = 0;
|
|
595
|
+
let balanced = true;
|
|
596
|
+
for (let i = 0; i < val.length - 1; i++) {
|
|
597
|
+
if (val[i] === '(') depth++;
|
|
598
|
+
else if (val[i] === ')') depth--;
|
|
599
|
+
if (depth === 0 && i < val.length - 1) {
|
|
600
|
+
balanced = false;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (!balanced) break;
|
|
605
|
+
val = val.slice(1, -1).trim();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// SQL 引号包裹的字符串:'hello' → 提取内容
|
|
609
|
+
if (val.startsWith("'") && val.endsWith("'")) {
|
|
610
|
+
return val.slice(1, -1).replace(/''/g, "'");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 数字
|
|
614
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
615
|
+
return parseFloat(val);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 其他(函数表达式如 getdate()、newid() 等)直接返回字符串
|
|
619
|
+
return val;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 清理 PostgreSQL 系统目录中的默认值表达式
|
|
624
|
+
*
|
|
625
|
+
* column_default 返回值如:
|
|
626
|
+
* - nextval('xxx_seq'::regclass) → 序列自增,不应作为 DEFAULT
|
|
627
|
+
* - 'hello'::character varying → 带类型后缀的字符串默认值
|
|
628
|
+
* - 1, true, now() 等
|
|
629
|
+
*/
|
|
630
|
+
private normalizePostgreSQLDefaultValue(raw: string | null): { value: any; isSerial: boolean } {
|
|
631
|
+
if (!raw) return { value: undefined, isSerial: false };
|
|
632
|
+
|
|
633
|
+
const val = raw.trim();
|
|
634
|
+
|
|
635
|
+
// 序列自增(SERIAL/BIGSERIAL 列)
|
|
636
|
+
if (val.startsWith('nextval(')) {
|
|
637
|
+
return { value: undefined, isSerial: true };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 去除 ::type 后缀(PostgreSQL 类型转换语法)
|
|
641
|
+
let cleaned = val.replace(/::[\w."\[\]]+(\[\])?$/, '');
|
|
642
|
+
|
|
643
|
+
// SQL 引号包裹的字符串
|
|
644
|
+
if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
645
|
+
return { value: cleaned.slice(1, -1).replace(/''/g, "'"), isSerial: false };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// 数字
|
|
649
|
+
if (/^-?\d+(\.\d+)?$/.test(cleaned)) {
|
|
650
|
+
return { value: parseFloat(cleaned), isSerial: false };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 布尔
|
|
654
|
+
if (cleaned.toLowerCase() === 'true') return { value: true, isSerial: false };
|
|
655
|
+
if (cleaned.toLowerCase() === 'false') return { value: false, isSerial: false };
|
|
656
|
+
|
|
657
|
+
// 其他(函数表达式)
|
|
658
|
+
return { value: cleaned, isSerial: false };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============ 通用方法 ============
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* 格式化默认值
|
|
665
|
+
*/
|
|
666
|
+
private formatDefaultValue(value: any, colType: string): string {
|
|
667
|
+
if (value === null || value === undefined) {
|
|
668
|
+
return 'NULL';
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (typeof value === 'number') {
|
|
672
|
+
return String(value);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (typeof value === 'boolean') {
|
|
676
|
+
return value ? 'TRUE' : 'FALSE';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (typeof value === 'string') {
|
|
680
|
+
const trimmed = value.trim();
|
|
681
|
+
// SQL 函数表达式(如 CURRENT_TIMESTAMP, GETDATE(), now())
|
|
682
|
+
// 标识符后跟 ( 视为函数调用,直接输出不加引号
|
|
683
|
+
if (/^[a-zA-Z_]+\s*\(/.test(trimmed)
|
|
684
|
+
|| ['CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'].includes(trimmed.toUpperCase())) {
|
|
685
|
+
return trimmed;
|
|
686
|
+
}
|
|
687
|
+
return `'${this.escapeString(value)}'`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return String(value);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* 转义字符串
|
|
695
|
+
*/
|
|
696
|
+
private escapeString(str: string): string {
|
|
697
|
+
return str.replace(/'/g, "''");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* 执行建表 SQL
|
|
702
|
+
*/
|
|
703
|
+
private async executeCreateSql(
|
|
704
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType },
|
|
705
|
+
createSql: string
|
|
706
|
+
): Promise<void> {
|
|
707
|
+
const sqlCfgModel = {
|
|
708
|
+
isNativeSQL: true,
|
|
709
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
710
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
711
|
+
executeSql: createSql,
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
await this.crudPro.executeSQL(sqlCfgModel);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* 复制索引(可选功能)
|
|
719
|
+
*
|
|
720
|
+
* 从基表复制索引到分表。
|
|
721
|
+
* 注意:主键索引已在建表时创建,此处只复制普通索引。
|
|
722
|
+
*/
|
|
723
|
+
private async copyIndexes(
|
|
724
|
+
tableName: string,
|
|
725
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
726
|
+
): Promise<void> {
|
|
727
|
+
try {
|
|
728
|
+
const indexDefs = await this.getIndexDefinitions(baseCfg);
|
|
729
|
+
|
|
730
|
+
for (const indexDef of indexDefs) {
|
|
731
|
+
// 跳过主键索引(已创建)
|
|
732
|
+
if (indexDef.isPrimary || !indexDef.createSql) continue;
|
|
733
|
+
|
|
734
|
+
// 替换表名(支持不同数据库的标识符语法)
|
|
735
|
+
const indexSql = this.replaceTableNameInIndexSql(
|
|
736
|
+
indexDef.createSql,
|
|
737
|
+
tableName,
|
|
738
|
+
baseCfg.sqlDbType
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// 重命名索引以避免冲突
|
|
742
|
+
const renamedSql = this.renameIndex(indexSql, tableName, baseCfg.sqlDbType);
|
|
743
|
+
|
|
744
|
+
await this.executeCreateSql(baseCfg, renamedSql);
|
|
745
|
+
}
|
|
746
|
+
} catch (error) {
|
|
747
|
+
console.warn('[ShardingTableCreator] 复制索引失败:', error);
|
|
748
|
+
// 索引复制失败不影响主流程
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* 在索引 SQL 中替换表名
|
|
754
|
+
*
|
|
755
|
+
* 支持 MySQL(反引号)、PostgreSQL(双引号)、SQL Server(方括号) 的标识符语法
|
|
756
|
+
*/
|
|
757
|
+
private replaceTableNameInIndexSql(
|
|
758
|
+
indexSql: string,
|
|
759
|
+
tableName: string,
|
|
760
|
+
sqlDbType: SqlDbType
|
|
761
|
+
): string {
|
|
762
|
+
const baseTable = this.config.baseTable;
|
|
763
|
+
|
|
764
|
+
if (sqlDbType === SqlDbType.mysql) {
|
|
765
|
+
// MySQL: 反引号包裹
|
|
766
|
+
return indexSql.replace(
|
|
767
|
+
new RegExp(`\\\`${this.escapeRegExp(baseTable)}\\\``, 'g'),
|
|
768
|
+
`\`${tableName}\``
|
|
769
|
+
);
|
|
770
|
+
} else if (sqlDbType === SqlDbType.postgres) {
|
|
771
|
+
// PostgreSQL: 双引号包裹
|
|
772
|
+
return indexSql.replace(
|
|
773
|
+
new RegExp(`"${this.escapeRegExp(baseTable)}"`, 'g'),
|
|
774
|
+
`"${tableName}"`
|
|
775
|
+
);
|
|
776
|
+
} else if (sqlDbType === SqlDbType.sqlserver) {
|
|
777
|
+
// SQL Server: 方括号包裹
|
|
778
|
+
return indexSql.replace(
|
|
779
|
+
new RegExp(`\\[${this.escapeRegExp(baseTable)}\\]`, 'g'),
|
|
780
|
+
`[${tableName}]`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return indexSql;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* 获取基表的索引定义
|
|
789
|
+
*
|
|
790
|
+
* 支持 MySQL、PostgreSQL、SQL Server,返回可直接执行的 CREATE INDEX 语句
|
|
791
|
+
*/
|
|
792
|
+
private async getIndexDefinitions(
|
|
793
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
794
|
+
): Promise<Array<{ isPrimary: boolean; createSql: string }>> {
|
|
795
|
+
const { sqlDbType } = baseCfg;
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
if (sqlDbType === SqlDbType.mysql) {
|
|
799
|
+
return this.getMySQLIndexDefinitions(baseCfg);
|
|
800
|
+
} else if (sqlDbType === SqlDbType.postgres) {
|
|
801
|
+
return this.getPostgreSQLIndexDefinitions(baseCfg);
|
|
802
|
+
} else if (sqlDbType === SqlDbType.sqlserver) {
|
|
803
|
+
return this.getSQLServerIndexDefinitions(baseCfg);
|
|
804
|
+
}
|
|
805
|
+
} catch (error) {
|
|
806
|
+
console.warn('[ShardingTableCreator] 获取索引定义失败:', error);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return [];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* 获取 MySQL 索引定义
|
|
814
|
+
*/
|
|
815
|
+
private async getMySQLIndexDefinitions(
|
|
816
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
817
|
+
): Promise<Array<{ isPrimary: boolean; createSql: string }>> {
|
|
818
|
+
const sql = `
|
|
819
|
+
SELECT
|
|
820
|
+
INDEX_NAME,
|
|
821
|
+
COLUMN_NAME,
|
|
822
|
+
NON_UNIQUE,
|
|
823
|
+
INDEX_TYPE,
|
|
824
|
+
SEQ_IN_INDEX
|
|
825
|
+
FROM information_schema.STATISTICS
|
|
826
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
827
|
+
AND TABLE_NAME = '${this.config.baseTable}'
|
|
828
|
+
ORDER BY INDEX_NAME, SEQ_IN_INDEX
|
|
829
|
+
`;
|
|
830
|
+
|
|
831
|
+
const result = await this.crudPro.executeSQL({
|
|
832
|
+
isNativeSQL: true,
|
|
833
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
834
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
835
|
+
executeSql: sql,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const rows = result?.rows || [];
|
|
839
|
+
|
|
840
|
+
// 按索引名分组
|
|
841
|
+
const indexMap = new Map<string, { columns: string[]; isUnique: boolean; isPrimary: boolean }>();
|
|
842
|
+
|
|
843
|
+
for (const row of rows) {
|
|
844
|
+
const indexName = row.INDEX_NAME;
|
|
845
|
+
const columnName = row.COLUMN_NAME;
|
|
846
|
+
const isUnique = row.NON_UNIQUE === 0;
|
|
847
|
+
const isPrimary = indexName === 'PRIMARY';
|
|
848
|
+
|
|
849
|
+
if (!indexMap.has(indexName)) {
|
|
850
|
+
indexMap.set(indexName, { columns: [], isUnique, isPrimary });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
indexMap.get(indexName)!.columns.push(columnName);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// 生成 CREATE INDEX 语句
|
|
857
|
+
const definitions: Array<{ isPrimary: boolean; createSql: string }> = [];
|
|
858
|
+
|
|
859
|
+
for (const [indexName, info] of indexMap) {
|
|
860
|
+
const { columns, isUnique, isPrimary } = info;
|
|
861
|
+
|
|
862
|
+
if (isPrimary) {
|
|
863
|
+
// 主键索引在建表时已创建,标记为主键但不需要执行 SQL
|
|
864
|
+
definitions.push({ isPrimary: true, createSql: '' });
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const columnList = columns.map(c => `\`${c}\``).join(', ');
|
|
869
|
+
const uniqueKeyword = isUnique ? 'UNIQUE ' : '';
|
|
870
|
+
const createSql = `CREATE ${uniqueKeyword}INDEX \`${indexName}\` ON \`${this.config.baseTable}\` (${columnList})`;
|
|
871
|
+
|
|
872
|
+
definitions.push({ isPrimary: false, createSql });
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return definitions;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* 获取 PostgreSQL 索引定义
|
|
880
|
+
*/
|
|
881
|
+
private async getPostgreSQLIndexDefinitions(
|
|
882
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
883
|
+
): Promise<Array<{ isPrimary: boolean; createSql: string }>> {
|
|
884
|
+
const schema = 'public';
|
|
885
|
+
|
|
886
|
+
// 查询索引定义
|
|
887
|
+
const sql = `
|
|
888
|
+
SELECT
|
|
889
|
+
i.relname as index_name,
|
|
890
|
+
ix.indisprimary as is_primary,
|
|
891
|
+
ix.indisunique as is_unique,
|
|
892
|
+
pg_get_indexdef(ix.indexrelid) as index_def
|
|
893
|
+
FROM pg_index ix
|
|
894
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
895
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
896
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
897
|
+
WHERE n.nspname = '${schema}'
|
|
898
|
+
AND t.relname = '${this.config.baseTable}'
|
|
899
|
+
`;
|
|
900
|
+
|
|
901
|
+
const result = await this.crudPro.executeSQL({
|
|
902
|
+
isNativeSQL: true,
|
|
903
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
904
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
905
|
+
executeSql: sql,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const rows = result?.rows || [];
|
|
909
|
+
|
|
910
|
+
return rows.map((row: any) => ({
|
|
911
|
+
isPrimary: row.is_primary,
|
|
912
|
+
createSql: row.index_def,
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* 获取 SQL Server 索引定义
|
|
918
|
+
*/
|
|
919
|
+
private async getSQLServerIndexDefinitions(
|
|
920
|
+
baseCfg: { sqlDatabase: string; sqlDbType: SqlDbType }
|
|
921
|
+
): Promise<Array<{ isPrimary: boolean; createSql: string }>> {
|
|
922
|
+
const sql = `
|
|
923
|
+
SELECT
|
|
924
|
+
i.name as index_name,
|
|
925
|
+
i.is_primary_key as is_primary,
|
|
926
|
+
i.is_unique as is_unique,
|
|
927
|
+
i.type_desc as index_type,
|
|
928
|
+
STUFF((
|
|
929
|
+
SELECT ', [' + c.name + ']'
|
|
930
|
+
FROM sys.index_columns ic
|
|
931
|
+
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
|
|
932
|
+
WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id
|
|
933
|
+
ORDER BY ic.key_ordinal
|
|
934
|
+
FOR XML PATH('')
|
|
935
|
+
), 1, 2, '') as column_list
|
|
936
|
+
FROM sys.indexes i
|
|
937
|
+
WHERE i.object_id = OBJECT_ID('${this.config.baseTable}')
|
|
938
|
+
AND i.name IS NOT NULL
|
|
939
|
+
`;
|
|
940
|
+
|
|
941
|
+
const result = await this.crudPro.executeSQL({
|
|
942
|
+
isNativeSQL: true,
|
|
943
|
+
sqlDatabase: baseCfg.sqlDatabase,
|
|
944
|
+
sqlDbType: baseCfg.sqlDbType,
|
|
945
|
+
executeSql: sql,
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
const rows = result?.rows || [];
|
|
949
|
+
|
|
950
|
+
return rows.map((row: any) => {
|
|
951
|
+
const isPrimary = row.is_primary;
|
|
952
|
+
const isUnique = row.is_unique;
|
|
953
|
+
const indexName = row.index_name;
|
|
954
|
+
const columnList = row.column_list;
|
|
955
|
+
const indexType = row.index_type === 'NONCLUSTERED' ? 'NONCLUSTERED' : 'CLUSTERED';
|
|
956
|
+
|
|
957
|
+
if (isPrimary) {
|
|
958
|
+
return { isPrimary: true, createSql: '' };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const uniqueKeyword = isUnique ? 'UNIQUE ' : '';
|
|
962
|
+
const createSql = `CREATE ${uniqueKeyword}${indexType} INDEX [${indexName}] ON [${this.config.baseTable}] (${columnList})`;
|
|
963
|
+
|
|
964
|
+
return { isPrimary: false, createSql };
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* 重命名索引(避免不同分表索引名冲突)
|
|
970
|
+
*
|
|
971
|
+
* 支持 MySQL、PostgreSQL、SQL Server 的标识符语法
|
|
972
|
+
*/
|
|
973
|
+
private renameIndex(indexSql: string, tableName: string, sqlDbType: SqlDbType): string {
|
|
974
|
+
// 提取表名后缀(如 202403)
|
|
975
|
+
const suffix = tableName.replace(this.config.baseTable + '_', '');
|
|
976
|
+
|
|
977
|
+
// 根据不同数据库类型使用不同的标识符包裹方式
|
|
978
|
+
let indexPattern: RegExp;
|
|
979
|
+
let replacement: string;
|
|
980
|
+
|
|
981
|
+
if (sqlDbType === SqlDbType.mysql) {
|
|
982
|
+
// MySQL: 反引号包裹
|
|
983
|
+
indexPattern = /INDEX `([^`]+)`/i;
|
|
984
|
+
replacement = `INDEX \`$1_${suffix}\``;
|
|
985
|
+
} else if (sqlDbType === SqlDbType.postgres) {
|
|
986
|
+
// PostgreSQL: 双引号包裹
|
|
987
|
+
indexPattern = /INDEX "([^"]+)"/i;
|
|
988
|
+
replacement = `INDEX "$1_${suffix}"`;
|
|
989
|
+
} else if (sqlDbType === SqlDbType.sqlserver) {
|
|
990
|
+
// SQL Server: 方括号包裹
|
|
991
|
+
indexPattern = /INDEX \[([^\]]+)\]/i;
|
|
992
|
+
replacement = `INDEX [$1_${suffix}]`;
|
|
993
|
+
} else {
|
|
994
|
+
// 默认:无包裹(不太可能到达这里)
|
|
995
|
+
return indexSql;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return indexSql.replace(indexPattern, replacement);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* 转义正则特殊字符
|
|
1003
|
+
*/
|
|
1004
|
+
private escapeRegExp(str: string): string {
|
|
1005
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1006
|
+
}
|
|
1007
|
+
}
|