303 lines
13 KiB
Java
303 lines
13 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.ResultSet;
|
|
import java.sql.Statement;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Properties;
|
|
import java.util.Set;
|
|
import java.util.HashSet;
|
|
|
|
/** Applies the module cutover and runtime views with prerequisite and completion checks. */
|
|
public final class ApplyModuleRuntimeViews {
|
|
private static final List<String> REQUIRED_TABLES = List.of(
|
|
"s_module",
|
|
"s_module_page",
|
|
"s_module_data",
|
|
"s_module_report",
|
|
"s_module_dashboard",
|
|
"s_module_workflow",
|
|
"s_function_node",
|
|
"s_page_node",
|
|
"s_module_profile",
|
|
"s_module_field",
|
|
"s_module_field_list",
|
|
"s_module_field_form",
|
|
"s_module_field_query",
|
|
"s_module_action",
|
|
"b_role",
|
|
"b_user_role",
|
|
"b_role_function_node",
|
|
"b_role_page_node",
|
|
"b_role_action",
|
|
"b_user_function_node",
|
|
"b_user_page_node",
|
|
"b_user_action"
|
|
);
|
|
private static final List<String> REQUIRED_VIEWS = List.of(
|
|
"v_s_function_node",
|
|
"v_s_page_node",
|
|
"v_s_module_profile",
|
|
"v_s_module_runtime",
|
|
"v_s_module_field_list",
|
|
"v_s_module_field_form",
|
|
"v_s_module_field_query",
|
|
"v_s_module_action",
|
|
"v_s_user_function_permission",
|
|
"v_s_user_page_permission",
|
|
"v_s_user_action_permission"
|
|
);
|
|
private static final List<String> INSPECTION_TABLES = List.of(
|
|
"s_module",
|
|
"s_module_profile",
|
|
"s_function_node",
|
|
"s_page_node",
|
|
"s_module_field",
|
|
"s_module_field_list",
|
|
"s_module_field_edit",
|
|
"s_module_field_query",
|
|
"s_module_field_group",
|
|
"s_module_power",
|
|
"b_user_module",
|
|
"b_user_power"
|
|
);
|
|
public static void main(String[] args) throws Exception {
|
|
Path apiRoot = Path.of("").toAbsolutePath().normalize();
|
|
boolean hasModeArgument = args.length > 0 && args[0].startsWith("--");
|
|
String mode = hasModeArgument ? args[0] : "--views-only";
|
|
int configIndex = hasModeArgument ? 1 : 0;
|
|
Path configPath = args.length > configIndex
|
|
? apiRoot.resolve(args[configIndex]).normalize()
|
|
: apiRoot.resolve("config/dbconfigs/g3hd.properties");
|
|
Path cutoverPath = apiRoot.resolve("config/migrations/033_create_module_structure.sql");
|
|
Path migrationPath = apiRoot.resolve("config/migrations/034_module_runtime_views.sql");
|
|
requirePathWithin(apiRoot, configPath);
|
|
requirePathWithin(apiRoot, cutoverPath);
|
|
requirePathWithin(apiRoot, migrationPath);
|
|
|
|
Properties properties = new Properties();
|
|
try (InputStream input = Files.newInputStream(configPath)) {
|
|
properties.load(input);
|
|
}
|
|
|
|
try (Connection connection = DriverManager.getConnection(
|
|
required(properties, "url"),
|
|
required(properties, "username"),
|
|
required(properties, "password")
|
|
)) {
|
|
if (!"FMS".equalsIgnoreCase(connection.getCatalog())) {
|
|
throw new IllegalStateException("Refusing to migrate database: " + connection.getCatalog());
|
|
}
|
|
|
|
if (mode.equals("--dry-run-cutover")) {
|
|
List<String> cutoverTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES);
|
|
if (!cutoverTables.isEmpty()) {
|
|
executeCutover(connection, cutoverPath, true);
|
|
}
|
|
System.out.println("PASS|033_create_module_structure.sql|dry_run=true|committed=false");
|
|
return;
|
|
}
|
|
if (mode.equals("--apply-cutover")) {
|
|
List<String> cutoverTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES);
|
|
if (cutoverTables.isEmpty()) {
|
|
System.out.println("PASS|033_create_module_structure.sql|applied=false");
|
|
} else {
|
|
executeCutover(connection, cutoverPath, false);
|
|
cutoverTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES);
|
|
if (!cutoverTables.isEmpty()) {
|
|
throw new IllegalStateException(
|
|
"Migration 033 did not complete; tables remain missing: "
|
|
+ String.join(",", cutoverTables)
|
|
);
|
|
}
|
|
System.out.println("APPLIED|033_create_module_structure.sql");
|
|
}
|
|
} else if (!mode.equals("--views-only")) {
|
|
throw new IllegalArgumentException("Unknown mode: " + mode);
|
|
}
|
|
|
|
List<String> missingTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES);
|
|
if (!missingTables.isEmpty()) {
|
|
System.out.println("MISSING_PREREQUISITE_TABLES|" + String.join(",", missingTables));
|
|
printPartialState(connection);
|
|
throw new IllegalStateException("Run migration 033 before migration 034");
|
|
}
|
|
|
|
List<String> missingBefore = missingViews(connection);
|
|
if (missingBefore.isEmpty()) {
|
|
System.out.println("PASS|required_module_views=present|applied=false");
|
|
return;
|
|
}
|
|
System.out.println("MISSING|" + String.join(",", missingBefore));
|
|
|
|
connection.setAutoCommit(false);
|
|
try {
|
|
String migration = Files.readString(migrationPath, StandardCharsets.UTF_8);
|
|
int batches = executeBatches(connection, migration);
|
|
List<String> missingAfter = missingViews(connection);
|
|
if (!missingAfter.isEmpty()) {
|
|
throw new IllegalStateException(
|
|
"Required views remain missing: " + String.join(",", missingAfter)
|
|
);
|
|
}
|
|
connection.commit();
|
|
System.out.println("APPLIED|034_module_runtime_views.sql|batches=" + batches);
|
|
System.out.println("PASS|required_module_views=present|applied=true");
|
|
} catch (Exception exception) {
|
|
connection.rollback();
|
|
throw exception;
|
|
} finally {
|
|
connection.setAutoCommit(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static List<String> missingViews(Connection connection) throws Exception {
|
|
return missingObjects(connection, "sys.views", REQUIRED_VIEWS);
|
|
}
|
|
|
|
private static List<String> missingObjects(
|
|
Connection connection,
|
|
String catalogView,
|
|
List<String> requiredObjects
|
|
) throws Exception {
|
|
Set<String> existing = new HashSet<>();
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
|
|
"SELECT name FROM " + catalogView + " WHERE schema_id = SCHEMA_ID(N'dbo')"
|
|
)) {
|
|
while (rows.next()) existing.add(rows.getString(1));
|
|
}
|
|
List<String> missing = new ArrayList<>();
|
|
for (String required : requiredObjects) {
|
|
if (!existing.contains(required)) missing.add(required);
|
|
}
|
|
return missing;
|
|
}
|
|
|
|
private static int executeBatches(Connection connection, String migration) throws Exception {
|
|
String[] batches = migration.split("(?im)^\\s*GO\\s*(?:--.*)?$");
|
|
int executed = 0;
|
|
for (String batch : batches) {
|
|
if (batch.isBlank()) continue;
|
|
try (Statement statement = connection.createStatement()) {
|
|
boolean hasResult = statement.execute(batch);
|
|
while (true) {
|
|
if (hasResult) {
|
|
try (ResultSet rows = statement.getResultSet()) {
|
|
while (rows.next()) {
|
|
// Drain server results so the complete SQL batch finishes.
|
|
}
|
|
}
|
|
} else if (statement.getUpdateCount() == -1) {
|
|
break;
|
|
}
|
|
hasResult = statement.getMoreResults();
|
|
}
|
|
}
|
|
executed++;
|
|
}
|
|
return executed;
|
|
}
|
|
|
|
private static void executeCutover(Connection connection, Path migrationPath, boolean dryRun)
|
|
throws Exception {
|
|
connection.setAutoCommit(false);
|
|
String migration = Files.readString(migrationPath, StandardCharsets.UTF_8);
|
|
migration = migration.replaceAll(
|
|
"(?s)/\\* CUTOVER_INSTALLED_GUARD_START \\*/.*?"
|
|
+ "/\\* CUTOVER_INSTALLED_GUARD_END \\*/",
|
|
""
|
|
);
|
|
migration = migration.replaceAll(
|
|
"(?s)/\\* RETURN in the first batch means the architecture was already installed\\. \\*/"
|
|
+ "\\s*IF @@TRANCOUNT = 0\\s*RETURN;",
|
|
""
|
|
);
|
|
migration = migration.replaceAll("(?im)^\\s*BEGIN TRANSACTION;\\s*$", "");
|
|
migration = migration.replaceAll("(?im)^\\s*COMMIT TRANSACTION;\\s*$", "");
|
|
try {
|
|
printCutoverState(connection, "before");
|
|
int batches = executeBatches(connection, migration);
|
|
printCutoverState(connection, "after-sql");
|
|
List<String> missingTables = missingObjects(connection, "sys.tables", REQUIRED_TABLES);
|
|
if (!missingTables.isEmpty()) {
|
|
throw new IllegalStateException(
|
|
"Migration 033 did not create required tables: " + String.join(",", missingTables)
|
|
);
|
|
}
|
|
if (dryRun) {
|
|
connection.rollback();
|
|
} else {
|
|
connection.commit();
|
|
}
|
|
printCutoverState(connection, "after-jdbc");
|
|
System.out.println("CUTOVER_SQL|dry_run=" + dryRun + "|batches=" + batches);
|
|
} catch (Exception exception) {
|
|
try (Statement statement = connection.createStatement()) {
|
|
statement.execute("IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION");
|
|
}
|
|
connection.rollback();
|
|
throw exception;
|
|
} finally {
|
|
connection.setAutoCommit(true);
|
|
}
|
|
}
|
|
|
|
private static void printCutoverState(Connection connection, String phase) throws Exception {
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
|
|
"SELECT @@TRANCOUNT, CASE WHEN OBJECT_ID(N'dbo.s_module_page', N'U') IS NULL THEN 0 ELSE 1 END"
|
|
)) {
|
|
rows.next();
|
|
System.out.println("CUTOVER_STATE|" + phase + "|trancount=" + rows.getInt(1)
|
|
+ "|s_module_page=" + (rows.getInt(2) == 1));
|
|
}
|
|
}
|
|
|
|
|
|
private static void printPartialState(Connection connection) throws Exception {
|
|
Set<String> existing = new HashSet<>();
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
|
|
"SELECT name FROM sys.tables WHERE schema_id = SCHEMA_ID(N'dbo')"
|
|
)) {
|
|
while (rows.next()) existing.add(rows.getString(1));
|
|
}
|
|
for (String table : INSPECTION_TABLES) {
|
|
if (!existing.contains(table)) continue;
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
|
|
"SELECT COUNT_BIG(*) FROM dbo.[" + table.replace("]", "]]" ) + "]"
|
|
)) {
|
|
rows.next();
|
|
System.out.println("TABLE_STATE|" + table + "|rows=" + rows.getLong(1));
|
|
}
|
|
List<String> columns = new ArrayList<>();
|
|
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
|
|
"SELECT column_row.name, TYPE_NAME(column_row.user_type_id), column_row.is_nullable "
|
|
+ "FROM sys.columns AS column_row "
|
|
+ "WHERE column_row.object_id = OBJECT_ID(N'dbo.["
|
|
+ table.replace("]", "]]" )
|
|
+ "]') ORDER BY column_row.column_id"
|
|
)) {
|
|
while (rows.next()) {
|
|
columns.add(rows.getString(1) + ":" + rows.getString(2)
|
|
+ (rows.getBoolean(3) ? "?" : ""));
|
|
}
|
|
}
|
|
System.out.println("TABLE_COLUMNS|" + table + "|" + String.join(",", columns));
|
|
}
|
|
}
|
|
|
|
private static String required(Properties properties, String key) {
|
|
String value = properties.getProperty(key);
|
|
if (value == null || value.isBlank()) throw new IllegalStateException("Missing property: " + key);
|
|
return value.trim();
|
|
}
|
|
|
|
private static void requirePathWithin(Path root, Path path) {
|
|
if (!path.startsWith(root)) throw new IllegalArgumentException("Path is outside API root: " + path);
|
|
}
|
|
}
|