Files
workspace/code/fms/fms-api/config/migrations/tools/VerifyBusinessMigration.java
T
2026-09-14 22:49:48 +08:00

239 lines
11 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.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Performs read-only checks after the business schema migrations have run. */
public final class VerifyBusinessMigration {
private static final List<String> TABLE_MIGRATIONS = List.of(
"016_create_base_tables.sql",
"017_create_contract_file_tables.sql",
"018_create_business_tables.sql",
"019_create_finance_tables.sql",
"020_create_oa_tables.sql"
);
private static final Pattern CREATE_TABLE = Pattern.compile(
"(?i)CREATE\\s+TABLE\\s+dbo\\.(?:\\[([^]]+)]|([A-Za-z0-9_]+))"
);
public static void main(String[] args) throws Exception {
Path apiRoot = Path.of("").toAbsolutePath().normalize();
Path migrationDirectory = apiRoot.resolve("config/migrations");
Set<String> generatedTables = loadGeneratedTables(migrationDirectory);
Properties properties = new Properties();
try (InputStream input = Files.newInputStream(apiRoot.resolve("config/dbconfigs/G3HD.properties"))) {
properties.load(input);
}
try (Connection connection = DriverManager.getConnection(
properties.getProperty("url"),
properties.getProperty("username"),
properties.getProperty("password")
)) {
if (!"FMS".equalsIgnoreCase(connection.getCatalog())) {
throw new IllegalStateException("Refusing to verify database: " + connection.getCatalog());
}
verifyGeneratedTables(connection, generatedTables);
verifyCoreTables(connection);
verifyViews(connection);
printCatalogCounts(connection, generatedTables.size());
}
}
private static Set<String> loadGeneratedTables(Path migrationDirectory) throws Exception {
Set<String> result = new LinkedHashSet<>();
for (String migration : TABLE_MIGRATIONS) {
String sql = Files.readString(migrationDirectory.resolve(migration), StandardCharsets.UTF_8);
Matcher matcher = CREATE_TABLE.matcher(sql);
while (matcher.find()) {
result.add(matcher.group(1) != null ? matcher.group(1) : matcher.group(2));
}
}
return result;
}
private static void verifyGeneratedTables(Connection connection, Set<String> tables) throws Exception {
for (String table : tables) {
if (!tableExists(connection, table)) {
throw new IllegalStateException("Missing generated table: dbo." + table);
}
if (rowCount(connection, table) != 0) {
throw new IllegalStateException("Generated table is not empty: dbo." + table);
}
String primaryKey = columnExists(connection, table, "subid") ? "subid" : "b_id";
if (!primaryKeyMatches(connection, table, primaryKey)) {
throw new IllegalStateException("Unexpected primary key on dbo." + table + "; expected " + primaryKey);
}
verifyIdTypes(connection, table);
verifyNoLegacyTypes(connection, table);
verifyRemovedPluralIds(connection, table);
}
System.out.println("PASS|generated_tables=" + tables.size() + "|empty=true|keys=true|types=true");
}
private static void verifyCoreTables(Connection connection) throws Exception {
for (String table : List.of("b_othercompany", "b_contact", "b_othercompany_category")) {
if (!tableExists(connection, table)) {
throw new IllegalStateException("Missing core business table: dbo." + table);
}
verifyIdTypes(connection, table);
}
long users = scalar(connection, "SELECT COUNT_BIG(*) FROM dbo.b_user");
long modules = scalar(connection, "SELECT COUNT_BIG(*) FROM dbo.s_module");
long fields = scalar(connection, "SELECT COUNT_BIG(*) FROM dbo.s_module_field");
long autoCodes = scalar(connection, "SELECT COUNT_BIG(*) FROM dbo.s_module_auto_code");
if (users == 0 || modules == 0 || fields == 0) {
throw new IllegalStateException("Existing user or module metadata is missing");
}
System.out.println("PASS|preserved|users=" + users + "|modules=" + modules
+ "|fields=" + fields + "|auto_codes=" + autoCodes);
}
private static void verifyViews(Connection connection) throws Exception {
List<String> viewNames = new ArrayList<>();
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
"SELECT QUOTENAME(SCHEMA_NAME(schema_id)) + N'.' + QUOTENAME(name) "
+ "FROM sys.views WHERE is_ms_shipped = 0 ORDER BY name"
)) {
while (rows.next()) {
viewNames.add(rows.getString(1));
}
}
for (String viewName : viewNames) {
try (Statement refresh = connection.createStatement()) {
refresh.execute("EXEC sys.sp_refreshview N'" + viewName.replace("'", "''") + "'");
}
}
System.out.println("PASS|views_refreshed=" + viewNames.size());
}
private static void printCatalogCounts(Connection connection, int generatedTableCount) throws Exception {
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
"SELECT (SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0), "
+ "(SELECT COUNT(*) FROM sys.views WHERE is_ms_shipped = 0)"
)) {
rows.next();
System.out.println("PASS|catalog|tables=" + rows.getInt(1) + "|views=" + rows.getInt(2)
+ "|generated_tables=" + generatedTableCount);
}
}
private static boolean tableExists(Connection connection, String table) throws Exception {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT COUNT(*) FROM sys.tables WHERE schema_id = SCHEMA_ID(N'dbo') AND name = ?"
)) {
statement.setString(1, table);
try (ResultSet rows = statement.executeQuery()) {
rows.next();
return rows.getInt(1) == 1;
}
}
}
private static boolean columnExists(Connection connection, String table, String column) throws Exception {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT COUNT(*) FROM sys.columns WHERE object_id = OBJECT_ID(?) AND name = ?"
)) {
statement.setString(1, "dbo." + table);
statement.setString(2, column);
try (ResultSet rows = statement.executeQuery()) {
rows.next();
return rows.getInt(1) == 1;
}
}
}
private static boolean primaryKeyMatches(Connection connection, String table, String expected) throws Exception {
String sql = """
SELECT c.name
FROM sys.indexes AS i
INNER JOIN sys.index_columns AS ic
ON ic.object_id = i.object_id AND ic.index_id = i.index_id
INNER JOIN sys.columns AS c
ON c.object_id = ic.object_id AND c.column_id = ic.column_id
WHERE i.object_id = OBJECT_ID(?) AND i.is_primary_key = 1
ORDER BY ic.key_ordinal
""";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "dbo." + table);
try (ResultSet rows = statement.executeQuery()) {
return rows.next()
&& rows.getString(1).equalsIgnoreCase(expected)
&& !rows.next();
}
}
}
private static void verifyIdTypes(Connection connection, String table) throws Exception {
String sql = """
SELECT c.name, TYPE_NAME(c.user_type_id)
FROM sys.columns AS c
WHERE c.object_id = OBJECT_ID(?)
AND (LOWER(c.name) IN (N'b_id', N'subid', N'father') OR LOWER(c.name) LIKE N'%[_]id')
AND TYPE_NAME(c.user_type_id) <> N'bigint'
""";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "dbo." + table);
try (ResultSet rows = statement.executeQuery()) {
if (rows.next()) {
throw new IllegalStateException("Non-bigint ID column: dbo." + table + "."
+ rows.getString(1) + " (" + rows.getString(2).toLowerCase(Locale.ROOT) + ")");
}
}
}
}
private static void verifyNoLegacyTypes(Connection connection, String table) throws Exception {
String sql = """
SELECT c.name, TYPE_NAME(c.user_type_id)
FROM sys.columns AS c
WHERE c.object_id = OBJECT_ID(?)
AND TYPE_NAME(c.user_type_id) IN (N'text', N'ntext', N'image', N'float', N'real', N'uniqueidentifier')
""";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "dbo." + table);
try (ResultSet rows = statement.executeQuery()) {
if (rows.next()) {
throw new IllegalStateException("Legacy type remains: dbo." + table + "." + rows.getString(1));
}
}
}
}
private static void verifyRemovedPluralIds(Connection connection, String table) throws Exception {
for (String column : List.of("b_yw_ids", "b_bill_ids", "b_inv_ids")) {
if (columnExists(connection, table, column)) {
throw new IllegalStateException("Legacy plural-ID column remains: dbo." + table + "." + column);
}
}
}
private static long rowCount(Connection connection, String table) throws Exception {
return scalar(connection, "SELECT COUNT_BIG(*) FROM dbo.[" + table.replace("]", "]]" ) + "]");
}
private static long scalar(Connection connection, String sql) throws Exception {
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(sql)) {
rows.next();
return rows.getLong(1);
}
}
}