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 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 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 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 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 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 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 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 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 missingViews(Connection connection) throws Exception { return missingObjects(connection, "sys.views", REQUIRED_VIEWS); } private static List missingObjects( Connection connection, String catalogView, List requiredObjects ) throws Exception { Set 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 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 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 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 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); } }