634 lines
31 KiB
Java
634 lines
31 KiB
Java
import java.io.InputStream;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.ResultSet;
|
|
import java.sql.Statement;
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Properties;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Reads the legacy SQL Server catalog and emits reviewed, schema-only FMS migrations.
|
|
* The generated SQL never connects back to the legacy database and never contains data.
|
|
*/
|
|
public final class GenerateBusinessMigrations {
|
|
private static final Set<String> EXCLUDED_TABLES = lowerSet(
|
|
"b_contact", "b_contact_type", "b_othercompany", "b_othercompany_bmfl",
|
|
"b_user", "b_user_power", "b_user_qx", "b_user_record",
|
|
"dtproperties", "log_datas"
|
|
);
|
|
private static final Set<String> TREE_TABLES = lowerSet(
|
|
"b_other_bmfl", "bc_pricelist_bmfl", "oa_title_bmfl"
|
|
);
|
|
private static final Set<String> EXCLUDED_VIEWS = lowerSet(
|
|
"v_autoidrule", "v_b_contact", "v_b_employee", "v_b_user_record"
|
|
);
|
|
private static final Set<String> BOOLEAN_COLUMNS = lowerSet(
|
|
"b_black", "b_canuse", "b_visible", "b_used", "b_default", "b_isdefault",
|
|
"b_finish", "b_sh", "b_bill", "b_zx", "b_toall", "b_reminder",
|
|
"b_othermachinelogin", "b_salesman", "b_docman", "b_dealman",
|
|
"b_visible_customer", "b_visible_supply", "b_levelvisible", "b_fromcustomer",
|
|
"b_logintime", "b_checkxy", "b_loginfee", "mx_finish", "mx_sh"
|
|
);
|
|
private static final Set<String> TRUE_DEFAULT_COLUMNS = lowerSet(
|
|
"b_canuse", "b_visible", "b_used"
|
|
);
|
|
private static final Set<String> UNICODE_HINTS = lowerSet(
|
|
"name", "memo", "bz", "addr", "address", "subject", "content", "info",
|
|
"desc", "caption", "department", "duty", "bank", "goods", "mark",
|
|
"remark", "taste", "habit", "situation", "place", "body"
|
|
);
|
|
private static final Map<String, String> BUSINESS_CODE_FIELDS = Map.ofEntries(
|
|
Map.entry("bc_khht", "b_htno"),
|
|
Map.entry("bs_business", "b_ywno"),
|
|
Map.entry("cw_account", "b_account"),
|
|
Map.entry("cw_bill", "b_ywno"),
|
|
Map.entry("cw_invoice", "b_ywno"),
|
|
Map.entry("cw_sfd", "b_ywno")
|
|
);
|
|
private static final Map<String, String> CHILD_PARENTS = Map.ofEntries(
|
|
Map.entry("b_othercompany_accountbank", "b_othercompany"),
|
|
Map.entry("bc_pricelist_sub", "bc_pricelist"),
|
|
Map.entry("bs_business_container", "bs_business"),
|
|
Map.entry("bs_business_cw", "bs_business"),
|
|
Map.entry("bs_business_hdbl", "bs_business"),
|
|
Map.entry("bs_business_transport", "bs_business"),
|
|
Map.entry("cw_billsub", "cw_bill"),
|
|
Map.entry("cw_invoicesub", "cw_invoice"),
|
|
Map.entry("cw_sfdsub", "cw_sfd"),
|
|
Map.entry("oa_notices_attach", "oa_notices"),
|
|
Map.entry("oa_noticessub", "oa_notices")
|
|
);
|
|
private static final Map<String, String> RENAMED_COLUMNS = Map.ofEntries(
|
|
Map.entry("bs_business_cw.mx_type_id", "mx_direction"),
|
|
Map.entry("bs_business_transport.mx_hstype_id", "mx_source_type"),
|
|
Map.entry("cw_bill.b_module_id", "b_direction"),
|
|
Map.entry("cw_bill.b_type_id", "b_document_type"),
|
|
Map.entry("cw_invoice.b_module_id", "b_direction"),
|
|
Map.entry("cw_invoice.b_type_id", "b_document_type"),
|
|
Map.entry("cw_sfd.b_module_id", "b_direction"),
|
|
Map.entry("cw_sfd.b_type_id", "b_document_type")
|
|
);
|
|
|
|
private record Column(
|
|
String name,
|
|
String type,
|
|
int maxLength,
|
|
int precision,
|
|
int scale,
|
|
boolean nullable,
|
|
int ordinal
|
|
) {}
|
|
|
|
private record Table(String name, List<Column> columns) {}
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
Path apiRoot = Path.of("").toAbsolutePath().normalize();
|
|
Path sourceConfig = apiRoot.resolve("config/dbconfigs/G3HY2025.properties");
|
|
Path migrations = apiRoot.resolve("config/migrations");
|
|
|
|
Properties properties = new Properties();
|
|
try (InputStream input = Files.newInputStream(sourceConfig)) {
|
|
properties.load(input);
|
|
}
|
|
|
|
try (Connection connection = DriverManager.getConnection(
|
|
properties.getProperty("url"),
|
|
properties.getProperty("username"),
|
|
properties.getProperty("password")
|
|
)) {
|
|
Map<String, Table> tables = loadTables(connection);
|
|
writeDomain(migrations.resolve("016_create_base_tables.sql"), "基础资料", tables, "base");
|
|
writeDomain(
|
|
migrations.resolve("017_create_contract_file_tables.sql"),
|
|
"合同和文件资料",
|
|
tables,
|
|
"contract-file"
|
|
);
|
|
writeDomain(migrations.resolve("018_create_business_tables.sql"), "业务单据", tables, "business");
|
|
writeDomain(migrations.resolve("019_create_finance_tables.sql"), "财务", tables, "finance");
|
|
writeDomain(migrations.resolve("020_create_oa_tables.sql"), "OA", tables, "oa");
|
|
writeViews(migrations.resolve("021_create_business_views.sql"), connection);
|
|
writeVerification(migrations.resolve("verify_business_schema.sql"), tables, connection);
|
|
}
|
|
}
|
|
|
|
private static Map<String, Table> loadTables(Connection connection) throws Exception {
|
|
String sql = """
|
|
SELECT t.name AS table_name, c.name AS column_name, ty.name AS type_name,
|
|
c.max_length, c.precision, c.scale, c.is_nullable, c.column_id
|
|
FROM sys.tables AS t
|
|
INNER JOIN sys.schemas AS s ON s.schema_id = t.schema_id
|
|
INNER JOIN sys.columns AS c ON c.object_id = t.object_id
|
|
INNER JOIN sys.types AS ty ON ty.user_type_id = c.user_type_id
|
|
WHERE s.name = N'dbo' AND t.is_ms_shipped = 0
|
|
ORDER BY t.name, c.column_id
|
|
""";
|
|
Map<String, List<Column>> columnsByTable = new LinkedHashMap<>();
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(sql)) {
|
|
while (rows.next()) {
|
|
String tableName = rows.getString("table_name");
|
|
columnsByTable.computeIfAbsent(tableName, ignored -> new ArrayList<>()).add(new Column(
|
|
rows.getString("column_name"),
|
|
rows.getString("type_name"),
|
|
rows.getInt("max_length"),
|
|
rows.getInt("precision"),
|
|
rows.getInt("scale"),
|
|
rows.getBoolean("is_nullable"),
|
|
rows.getInt("column_id")
|
|
));
|
|
}
|
|
}
|
|
|
|
Map<String, Table> result = new LinkedHashMap<>();
|
|
for (Map.Entry<String, List<Column>> entry : columnsByTable.entrySet()) {
|
|
if (!includeTable(entry.getKey())) continue;
|
|
result.put(entry.getKey().toLowerCase(Locale.ROOT), new Table(entry.getKey(), entry.getValue()));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static boolean includeTable(String tableName) {
|
|
String name = tableName.toLowerCase(Locale.ROOT);
|
|
if (EXCLUDED_TABLES.contains(name)) return false;
|
|
return name.startsWith("b_")
|
|
|| name.startsWith("bc_")
|
|
|| name.startsWith("bf_")
|
|
|| name.startsWith("bs_")
|
|
|| name.startsWith("cw_")
|
|
|| name.startsWith("oa_");
|
|
}
|
|
|
|
private static String domainOf(String tableName) {
|
|
String name = tableName.toLowerCase(Locale.ROOT);
|
|
if (name.startsWith("bc_") || name.startsWith("bf_")) return "contract-file";
|
|
if (name.startsWith("bs_")) return "business";
|
|
if (name.startsWith("cw_")) return "finance";
|
|
if (name.startsWith("oa_")) return "oa";
|
|
return "base";
|
|
}
|
|
|
|
private static void writeDomain(
|
|
Path target,
|
|
String title,
|
|
Map<String, Table> tables,
|
|
String domain
|
|
) throws Exception {
|
|
StringBuilder sql = new StringBuilder(header(title));
|
|
tables.values().stream()
|
|
.filter(table -> domainOf(table.name()).equals(domain))
|
|
.sorted(Comparator.comparing(Table::name, String.CASE_INSENSITIVE_ORDER))
|
|
.forEach(table -> sql.append(createTable(table)));
|
|
if (domain.equals("finance")) appendFinanceRelations(sql);
|
|
sql.append("\nCOMMIT TRANSACTION;\n");
|
|
Files.writeString(target, sql.toString(), StandardCharsets.UTF_8);
|
|
}
|
|
|
|
private static String header(String title) {
|
|
return """
|
|
/* Generated schema-only migration: %s. No legacy data is copied. */
|
|
SET NOCOUNT ON;
|
|
SET XACT_ABORT ON;
|
|
|
|
IF DB_NAME() <> N'FMS'
|
|
THROW 51501, N'本迁移只能在 FMS 目标数据库执行。', 1;
|
|
|
|
BEGIN TRANSACTION;
|
|
|
|
""".formatted(title);
|
|
}
|
|
|
|
private static String createTable(Table table) {
|
|
String lowerName = table.name().toLowerCase(Locale.ROOT);
|
|
if (TREE_TABLES.contains(lowerName)) return createTreeTable(table.name());
|
|
|
|
boolean child = hasColumn(table, "subid");
|
|
String primaryKey = child ? originalColumnName(table, "subid") : "b_id";
|
|
boolean sourceHasStringId = !child && table.columns().stream().anyMatch(column ->
|
|
column.name().equalsIgnoreCase("b_id") && isTextType(column.type())
|
|
);
|
|
String businessCode = businessCodeField(table, sourceHasStringId);
|
|
|
|
List<String> definitions = new ArrayList<>();
|
|
if (!child && !hasColumn(table, "b_id")) {
|
|
definitions.add(" b_id bigint NOT NULL");
|
|
}
|
|
for (Column column : table.columns()) {
|
|
if (isPluralIdColumn(column.name())) continue;
|
|
String targetName = targetColumnName(table, column.name());
|
|
String definition = " " + quote(targetName) + " " + targetType(table, column);
|
|
boolean requiredCode = businessCode != null && column.name().equalsIgnoreCase(businessCode);
|
|
boolean requiredKey = column.name().equalsIgnoreCase(primaryKey)
|
|
|| (!child && column.name().equalsIgnoreCase("b_id"));
|
|
if (BOOLEAN_COLUMNS.contains(column.name().toLowerCase(Locale.ROOT))) {
|
|
int defaultValue = TRUE_DEFAULT_COLUMNS.contains(column.name().toLowerCase(Locale.ROOT)) ? 1 : 0;
|
|
definition += " NOT NULL CONSTRAINT "
|
|
+ quote("DF_" + safeName(table.name()) + "_" + safeName(column.name()))
|
|
+ " DEFAULT (" + defaultValue + ")";
|
|
} else {
|
|
definition += requiredCode || requiredKey ? " NOT NULL" : column.nullable() ? " NULL" : " NOT NULL";
|
|
}
|
|
definitions.add(definition);
|
|
}
|
|
if (sourceHasStringId && businessCode != null && !hasColumn(table, businessCode)) {
|
|
definitions.add(" " + quote(businessCode) + " varchar(50) NOT NULL");
|
|
}
|
|
definitions.add(" CONSTRAINT " + quote("PK_" + safeName(table.name()))
|
|
+ " PRIMARY KEY (" + quote(primaryKey) + ")");
|
|
if (businessCode != null) {
|
|
definitions.add(" CONSTRAINT " + quote("UQ_" + safeName(table.name()) + "_" + businessCode)
|
|
+ " UNIQUE (" + quote(businessCode) + ")");
|
|
}
|
|
String parentTable = CHILD_PARENTS.get(lowerName);
|
|
if (child && parentTable != null && hasColumn(table, "father")) {
|
|
definitions.add(" CONSTRAINT " + quote("FK_" + safeName(table.name()) + "_father")
|
|
+ " FOREIGN KEY (" + quote(originalColumnName(table, "father")) + ") REFERENCES dbo."
|
|
+ quote(parentTable) + " (b_id)");
|
|
}
|
|
|
|
StringBuilder result = new StringBuilder();
|
|
result.append(" IF OBJECT_ID(N'dbo.").append(escape(table.name())).append("', N'U') IS NULL\n");
|
|
result.append(" BEGIN\n");
|
|
result.append(" CREATE TABLE dbo.").append(quote(table.name())).append(" (\n");
|
|
result.append(String.join(",\n", definitions)).append("\n );\n");
|
|
appendIndexes(result, table, primaryKey, businessCode);
|
|
result.append(" END;\n\n");
|
|
return result.toString();
|
|
}
|
|
|
|
private static String createTreeTable(String tableName) {
|
|
String safe = safeName(tableName);
|
|
return """
|
|
IF OBJECT_ID(N'dbo.%s', N'U') IS NULL
|
|
BEGIN
|
|
CREATE TABLE dbo.%s (
|
|
b_id bigint NOT NULL,
|
|
b_parent_id bigint NULL,
|
|
b_name nvarchar(150) NOT NULL,
|
|
b_level int NOT NULL CONSTRAINT %s DEFAULT (1),
|
|
b_path varchar(1000) NOT NULL,
|
|
b_xh int NOT NULL CONSTRAINT %s DEFAULT (0),
|
|
b_canuse int NOT NULL CONSTRAINT %s DEFAULT (1),
|
|
b_inputuser_id bigint NULL,
|
|
b_inputdatetime datetime2(0) NOT NULL CONSTRAINT %s DEFAULT (SYSDATETIME()),
|
|
b_updateuser_id bigint NULL,
|
|
b_updatedatetime datetime2(0) NULL,
|
|
CONSTRAINT %s PRIMARY KEY (b_id),
|
|
CONSTRAINT %s CHECK (b_parent_id IS NULL OR b_parent_id <> b_id),
|
|
CONSTRAINT %s FOREIGN KEY (b_parent_id) REFERENCES dbo.%s (b_id)
|
|
);
|
|
CREATE INDEX %s ON dbo.%s (b_parent_id, b_xh, b_id);
|
|
CREATE INDEX %s ON dbo.%s (b_path, b_xh, b_id);
|
|
END;
|
|
|
|
""".formatted(
|
|
escape(tableName), quote(tableName),
|
|
quote("DF_" + safe + "_level"), quote("DF_" + safe + "_xh"),
|
|
quote("DF_" + safe + "_canuse"), quote("DF_" + safe + "_inputdatetime"),
|
|
quote("PK_" + safe), quote("CK_" + safe + "_parent"), quote("FK_" + safe + "_parent"),
|
|
quote(tableName), quote("IX_" + safe + "_parent"), quote(tableName),
|
|
quote("IX_" + safe + "_path"), quote(tableName)
|
|
);
|
|
}
|
|
|
|
private static void appendIndexes(
|
|
StringBuilder sql,
|
|
Table table,
|
|
String primaryKey,
|
|
String businessCode
|
|
) {
|
|
Set<String> indexed = new LinkedHashSet<>();
|
|
for (Column column : table.columns()) {
|
|
String targetName = targetColumnName(table, column.name());
|
|
String lower = targetName.toLowerCase(Locale.ROOT);
|
|
if (lower.equals(primaryKey.toLowerCase(Locale.ROOT))) continue;
|
|
if (businessCode != null && lower.equals(businessCode.toLowerCase(Locale.ROOT))) continue;
|
|
if (lower.equals("father") || (lower.endsWith("_id") && !isAuditUserColumn(lower))) {
|
|
if (!indexed.add(lower)) continue;
|
|
String indexName = "IX_" + safeName(table.name()) + "_" + safeName(column.name());
|
|
sql.append(" CREATE INDEX ").append(quote(indexName)).append(" ON dbo.")
|
|
.append(quote(table.name())).append(" (").append(quote(targetName))
|
|
.append(", ").append(quote(primaryKey)).append(");\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static String businessCodeField(Table table, boolean sourceHasStringId) {
|
|
String lowerName = table.name().toLowerCase(Locale.ROOT);
|
|
String configured = BUSINESS_CODE_FIELDS.get(lowerName);
|
|
if (configured != null) return configured;
|
|
if (hasColumn(table, "b_code")) return originalColumnName(table, "b_code");
|
|
return sourceHasStringId ? "b_code" : null;
|
|
}
|
|
|
|
private static String targetType(Table table, Column column) {
|
|
String name = column.name().toLowerCase(Locale.ROOT);
|
|
String sourceType = column.type().toLowerCase(Locale.ROOT);
|
|
String renameKey = table.name().toLowerCase(Locale.ROOT) + "." + name;
|
|
if (renameKey.equals("bs_business_cw.mx_type_id")
|
|
|| renameKey.equals("cw_bill.b_module_id")
|
|
|| renameKey.equals("cw_invoice.b_module_id")
|
|
|| renameKey.equals("cw_sfd.b_module_id")) {
|
|
return "varchar(20)";
|
|
}
|
|
if (renameKey.equals("bs_business_transport.mx_hstype_id")
|
|
|| renameKey.endsWith(".b_type_id")) {
|
|
return "varchar(50)";
|
|
}
|
|
if (name.equals("b_id") || name.equals("subid") || name.equals("father") || isReferenceId(name)) {
|
|
return "bigint";
|
|
}
|
|
if (BOOLEAN_COLUMNS.contains(name) || sourceType.equals("bit")) return "int";
|
|
if (name.endsWith("datetime") || name.endsWith("_datetime") || name.equals("b_inputdate")) {
|
|
return "datetime2(0)";
|
|
}
|
|
if (name.equals("b_inputtime")) return "time(0)";
|
|
if (name.equals("b_birthday") || name.endsWith("begindate") || name.endsWith("enddate")
|
|
|| name.endsWith("expiredate")) {
|
|
return "date";
|
|
}
|
|
if (sourceType.equals("text") || sourceType.equals("ntext")) return "nvarchar(max)";
|
|
if (sourceType.equals("image")) return "varbinary(max)";
|
|
if (sourceType.equals("datetime") || sourceType.equals("smalldatetime")) return "datetime2(0)";
|
|
if (sourceType.equals("float") || sourceType.equals("real")) return "decimal(18,6)";
|
|
if (sourceType.equals("money") || sourceType.equals("smallmoney")) return "decimal(19,4)";
|
|
if (sourceType.equals("decimal") || sourceType.equals("numeric")) {
|
|
return sourceType + "(" + column.precision() + "," + column.scale() + ")";
|
|
}
|
|
if (sourceType.equals("varchar") || sourceType.equals("char")
|
|
|| sourceType.equals("nvarchar") || sourceType.equals("nchar")) {
|
|
boolean unicode = sourceType.startsWith("n") || hasUnicodeHint(name);
|
|
String base = unicode ? "nvarchar" : "varchar";
|
|
int bytesPerChar = sourceType.startsWith("n") ? 2 : 1;
|
|
String length = column.maxLength() < 0 ? "max" : String.valueOf(column.maxLength() / bytesPerChar);
|
|
return base + "(" + length + ")";
|
|
}
|
|
if (sourceType.equals("binary") || sourceType.equals("varbinary")) {
|
|
String length = column.maxLength() < 0 ? "max" : String.valueOf(column.maxLength());
|
|
return sourceType + "(" + length + ")";
|
|
}
|
|
if (sourceType.equals("timestamp") || sourceType.equals("rowversion")) return "binary(8)";
|
|
if (sourceType.equals("uniqueidentifier")) return "bigint";
|
|
return sourceType;
|
|
}
|
|
|
|
private static boolean isReferenceId(String name) {
|
|
return name.endsWith("_id")
|
|
|| name.equals("userid")
|
|
|| name.equals("moduleid")
|
|
|| name.endsWith("userid")
|
|
|| name.endsWith("moduleid");
|
|
}
|
|
|
|
private static boolean isAuditUserColumn(String name) {
|
|
return name.equals("b_inputuser_id")
|
|
|| name.equals("b_updateuser_id")
|
|
|| name.equals("mx_inputuser_id")
|
|
|| name.equals("mx_updateuser_id");
|
|
}
|
|
|
|
private static boolean isPluralIdColumn(String name) {
|
|
String lower = name.toLowerCase(Locale.ROOT);
|
|
return lower.endsWith("_ids") || lower.equals("b_yw_ids") || lower.equals("b_bill_ids")
|
|
|| lower.equals("b_inv_ids");
|
|
}
|
|
|
|
private static boolean hasUnicodeHint(String name) {
|
|
for (String hint : UNICODE_HINTS) {
|
|
if (name.contains(hint)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static boolean isTextType(String type) {
|
|
String lower = type.toLowerCase(Locale.ROOT);
|
|
return lower.equals("varchar") || lower.equals("nvarchar") || lower.equals("char")
|
|
|| lower.equals("nchar") || lower.equals("text") || lower.equals("ntext");
|
|
}
|
|
|
|
private static boolean hasColumn(Table table, String name) {
|
|
return table.columns().stream().anyMatch(column -> column.name().equalsIgnoreCase(name));
|
|
}
|
|
|
|
private static String originalColumnName(Table table, String name) {
|
|
return table.columns().stream()
|
|
.filter(column -> column.name().equalsIgnoreCase(name))
|
|
.findFirst()
|
|
.map(Column::name)
|
|
.orElse(name);
|
|
}
|
|
|
|
private static void appendFinanceRelations(StringBuilder sql) {
|
|
appendRelation(sql, "cw_bill_business", "b_bill_id", "cw_bill", "b_business_id", "bs_business");
|
|
appendRelation(sql, "cw_invoice_bill", "b_invoice_id", "cw_invoice", "b_bill_id", "cw_bill");
|
|
appendRelation(sql, "cw_invoice_business", "b_invoice_id", "cw_invoice", "b_business_id", "bs_business");
|
|
appendRelation(sql, "cw_sfd_business", "b_sfd_id", "cw_sfd", "b_business_id", "bs_business");
|
|
appendRelation(sql, "cw_sfd_invoice", "b_sfd_id", "cw_sfd", "b_invoice_id", "cw_invoice");
|
|
appendRelation(sql, "cw_sfd_bill", "b_sfd_id", "cw_sfd", "b_bill_id", "cw_bill");
|
|
}
|
|
|
|
private static void appendRelation(
|
|
StringBuilder sql,
|
|
String table,
|
|
String leftColumn,
|
|
String leftTable,
|
|
String rightColumn,
|
|
String rightTable
|
|
) {
|
|
String safe = safeName(table);
|
|
sql.append(" IF OBJECT_ID(N'dbo.").append(table).append("', N'U') IS NULL\n")
|
|
.append(" BEGIN\n")
|
|
.append(" CREATE TABLE dbo.").append(quote(table)).append(" (\n")
|
|
.append(" subid bigint NOT NULL,\n")
|
|
.append(" ").append(quote(leftColumn)).append(" bigint NOT NULL,\n")
|
|
.append(" ").append(quote(rightColumn)).append(" bigint NOT NULL,\n")
|
|
.append(" b_xh int NOT NULL CONSTRAINT ").append(quote("DF_" + safe + "_xh"))
|
|
.append(" DEFAULT (0),\n")
|
|
.append(" CONSTRAINT ").append(quote("PK_" + safe)).append(" PRIMARY KEY (subid),\n")
|
|
.append(" CONSTRAINT ").append(quote("UQ_" + safe)).append(" UNIQUE (")
|
|
.append(quote(leftColumn)).append(", ").append(quote(rightColumn)).append("),\n")
|
|
.append(" CONSTRAINT ").append(quote("FK_" + safe + "_left")).append(" FOREIGN KEY (")
|
|
.append(quote(leftColumn)).append(") REFERENCES dbo.").append(quote(leftTable)).append(" (b_id),\n")
|
|
.append(" CONSTRAINT ").append(quote("FK_" + safe + "_right")).append(" FOREIGN KEY (")
|
|
.append(quote(rightColumn)).append(") REFERENCES dbo.").append(quote(rightTable)).append(" (b_id)\n")
|
|
.append(" );\n")
|
|
.append(" END;\n\n");
|
|
}
|
|
|
|
private static void writeViews(Path target, Connection connection) throws Exception {
|
|
String query = """
|
|
SELECT v.name, sm.definition
|
|
FROM sys.views AS v
|
|
INNER JOIN sys.schemas AS s ON s.schema_id = v.schema_id
|
|
INNER JOIN sys.sql_modules AS sm ON sm.object_id = v.object_id
|
|
WHERE s.name = N'dbo'
|
|
ORDER BY v.name
|
|
""";
|
|
StringBuilder sql = new StringBuilder(header("业务视图"));
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(query)) {
|
|
while (rows.next()) {
|
|
String name = rows.getString("name");
|
|
if (!includeView(name)) continue;
|
|
String definition = rows.getString("definition");
|
|
if (name.equalsIgnoreCase("v_b_othercompany_bmfl")) {
|
|
definition = "CREATE VIEW dbo.v_b_othercompany_bmfl AS "
|
|
+ "SELECT b_id, b_name FROM dbo.b_othercompany_bmfl";
|
|
} else if (name.equalsIgnoreCase("v_b_feetype")) {
|
|
definition = "CREATE VIEW dbo.v_b_feetype AS SELECT "
|
|
+ "fee.b_id, fee.b_name, fee.b_name_e, fee.b_bz, fee.b_default, "
|
|
+ "fee.b_amount, fee.b_tax, fee.b_money_id, fee.b_inputuser_id, "
|
|
+ "fee.b_inputdatetime, fee.b_updateuser_id, fee.b_updatedatetime, money.b_hl "
|
|
+ "FROM dbo.b_feetype AS fee "
|
|
+ "LEFT JOIN dbo.b_money AS money ON money.b_id = fee.b_money_id";
|
|
}
|
|
definition = removeLegacyPluralIds(definition);
|
|
definition = renameLegacySemanticIds(name, definition);
|
|
sql.append(" IF OBJECT_ID(N'dbo.").append(escape(name)).append("', N'V') IS NOT NULL\n")
|
|
.append(" DROP VIEW dbo.").append(quote(name)).append(";\n")
|
|
.append(" EXEC sys.sp_executesql N'")
|
|
.append(escape(definition))
|
|
.append("';\n\n");
|
|
}
|
|
}
|
|
sql.append("COMMIT TRANSACTION;\n");
|
|
Files.writeString(target, sql.toString(), StandardCharsets.UTF_8);
|
|
}
|
|
|
|
private static boolean includeView(String viewName) {
|
|
String name = viewName.toLowerCase(Locale.ROOT);
|
|
if (EXCLUDED_VIEWS.contains(name)) return false;
|
|
return name.startsWith("v_b_") || name.startsWith("v_bc_")
|
|
|| name.startsWith("v_bs_") || name.startsWith("v_cw_")
|
|
|| name.equals("v_feetype");
|
|
}
|
|
|
|
private static String removeLegacyPluralIds(String definition) {
|
|
String result = definition;
|
|
for (String field : List.of("b_yw_ids", "b_bill_ids", "b_inv_ids")) {
|
|
result = result.replaceAll(
|
|
"(?i)(?:dbo\\.)?[A-Za-z0-9_]+\\." + field + "\\s*,\\s*",
|
|
""
|
|
);
|
|
result = result.replaceAll(
|
|
"(?i),\\s*(?:dbo\\.)?[A-Za-z0-9_]+\\." + field + "(?=\\s+FROM)",
|
|
""
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static String renameLegacySemanticIds(String viewName, String definition) {
|
|
String result = definition;
|
|
result = replaceIgnoreCase(result, "dbo.bs_business_cw.mx_type_id", "dbo.bs_business_cw.mx_direction");
|
|
result = replaceIgnoreCase(result, "dbo.bs_business_transport.mx_hstype_id", "dbo.bs_business_transport.mx_source_type");
|
|
result = replaceIgnoreCase(result, "dbo.cw_bill.b_module_id", "dbo.cw_bill.b_direction");
|
|
result = replaceIgnoreCase(result, "dbo.cw_bill.b_type_id", "dbo.cw_bill.b_document_type");
|
|
result = replaceIgnoreCase(result, "dbo.cw_invoice.b_module_id", "dbo.cw_invoice.b_direction");
|
|
result = replaceIgnoreCase(result, "dbo.cw_invoice.b_type_id", "dbo.cw_invoice.b_document_type");
|
|
result = replaceIgnoreCase(result, "dbo.cw_sfd.b_module_id", "dbo.cw_sfd.b_direction");
|
|
result = replaceIgnoreCase(result, "dbo.cw_sfd.b_type_id", "dbo.cw_sfd.b_document_type");
|
|
if (viewName.equalsIgnoreCase("v_bs_business_cw")
|
|
|| viewName.equalsIgnoreCase("v_bs_business_cw_yf")
|
|
|| viewName.equalsIgnoreCase("v_bs_business_cw_ys")) {
|
|
result = replaceIgnoreCase(result, "mx_type_id", "mx_direction");
|
|
}
|
|
if (viewName.equalsIgnoreCase("v_cw_account")) {
|
|
result = replaceIgnoreCase(result, "b_type_id", "b_direction");
|
|
result = replaceIgnoreCase(
|
|
result,
|
|
"a.b_account_id = dbo.cw_account.b_account",
|
|
"a.b_account_id = dbo.cw_account.b_id"
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static String replaceIgnoreCase(String value, String source, String replacement) {
|
|
return value.replaceAll("(?i)" + java.util.regex.Pattern.quote(source), replacement);
|
|
}
|
|
|
|
private static String targetColumnName(Table table, String sourceName) {
|
|
String key = table.name().toLowerCase(Locale.ROOT) + "." + sourceName.toLowerCase(Locale.ROOT);
|
|
return RENAMED_COLUMNS.getOrDefault(key, sourceName);
|
|
}
|
|
|
|
private static void writeVerification(
|
|
Path target,
|
|
Map<String, Table> tables,
|
|
Connection connection
|
|
) throws Exception {
|
|
int expectedTables = tables.size() + 6 + 5;
|
|
int expectedViews = 0;
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
|
|
"SELECT name FROM sys.views WHERE is_ms_shipped = 0"
|
|
)) {
|
|
while (rows.next()) if (includeView(rows.getString(1))) expectedViews++;
|
|
}
|
|
String sql = """
|
|
SET NOCOUNT ON;
|
|
|
|
IF DB_NAME() <> N'FMS'
|
|
THROW 51510, N'验证脚本只能在 FMS 目标数据库执行。', 1;
|
|
|
|
SELECT
|
|
(SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0) AS table_count,
|
|
(SELECT COUNT(*) FROM sys.views WHERE is_ms_shipped = 0) AS view_count,
|
|
%d AS generated_business_table_minimum,
|
|
%d AS generated_business_view_count;
|
|
|
|
SELECT OBJECT_SCHEMA_NAME(c.object_id) AS schema_name,
|
|
OBJECT_NAME(c.object_id) AS object_name,
|
|
c.name AS column_name,
|
|
TYPE_NAME(c.user_type_id) AS type_name
|
|
FROM sys.columns AS c
|
|
INNER JOIN sys.tables AS t ON t.object_id = c.object_id
|
|
WHERE t.is_ms_shipped = 0
|
|
AND (c.name = N'b_id' OR c.name = N'subid' OR c.name = N'father' OR c.name LIKE N'%%[_]id')
|
|
AND TYPE_NAME(c.user_type_id) <> N'bigint'
|
|
ORDER BY schema_name, object_name, c.column_id;
|
|
|
|
SELECT s.name AS schema_name, v.name AS view_name
|
|
FROM sys.views AS v
|
|
INNER JOIN sys.schemas AS s ON s.schema_id = v.schema_id
|
|
WHERE v.is_ms_shipped = 0 AND OBJECTPROPERTY(v.object_id, 'IsSchemaBound') IN (0, 1)
|
|
AND EXISTS (
|
|
SELECT 1 FROM sys.sql_expression_dependencies AS d
|
|
WHERE d.referencing_id = v.object_id AND d.referenced_id IS NULL
|
|
AND d.referenced_entity_name IS NOT NULL
|
|
)
|
|
ORDER BY s.name, v.name;
|
|
""".formatted(expectedTables, expectedViews);
|
|
Files.writeString(target, sql, StandardCharsets.UTF_8);
|
|
}
|
|
|
|
private static String quote(String identifier) {
|
|
return "[" + identifier.replace("]", "]]" ) + "]";
|
|
}
|
|
|
|
private static String escape(String value) {
|
|
return value.replace("'", "''");
|
|
}
|
|
|
|
private static String safeName(String value) {
|
|
return value.replaceAll("[^A-Za-z0-9_]", "_");
|
|
}
|
|
|
|
private static Set<String> lowerSet(String... values) {
|
|
Set<String> result = new HashSet<>();
|
|
for (String value : values) result.add(value.toLowerCase(Locale.ROOT));
|
|
return result;
|
|
}
|
|
}
|