20260804173126
This commit is contained in:
1 parent
e17bbe9db3
commit
75b1ccaa6d
36 files changed
+3521
-718
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,65 @@
|
||||
[INFO] Scanning for projects...
|
||||
[INFO]
|
||||
[INFO] -------------------------< cn.g3soft:fms-api >--------------------------
|
||||
[INFO] Building fms-api 0.0.1-SNAPSHOT
|
||||
[INFO] from pom.xml
|
||||
[INFO] --------------------------------[ jar ]---------------------------------
|
||||
[INFO]
|
||||
[INFO] >>> spring-boot-maven-plugin:4.1.0:run (default-cli) > test-compile @ fms-api >>>
|
||||
[INFO]
|
||||
[INFO] --- maven-resources-plugin:3.5.0:resources (default-resources) @ fms-api ---
|
||||
[INFO] Copying 1 resource from src\main\resources to target\classes
|
||||
[INFO] Copying 0 resource from src\main\resources to target\classes
|
||||
[INFO]
|
||||
[INFO] --- maven-compiler-plugin:3.15.0:compile (default-compile) @ fms-api ---
|
||||
[INFO] Nothing to compile - all classes are up to date.
|
||||
[INFO]
|
||||
[INFO] --- maven-resources-plugin:3.5.0:testResources (default-testResources) @ fms-api ---
|
||||
[INFO] skip non existing resourceDirectory D:\workspace\code\fms\fms-api\src\test\resources
|
||||
[INFO]
|
||||
[INFO] --- maven-compiler-plugin:3.15.0:testCompile (default-testCompile) @ fms-api ---
|
||||
[INFO] Nothing to compile - all classes are up to date.
|
||||
[INFO]
|
||||
[INFO] <<< spring-boot-maven-plugin:4.1.0:run (default-cli) < test-compile @ fms-api <<<
|
||||
[INFO]
|
||||
[INFO]
|
||||
[INFO] --- spring-boot-maven-plugin:4.1.0:run (default-cli) @ fms-api ---
|
||||
[INFO] Attaching agents: []
|
||||
|
||||
. ____ _ __ _ _
|
||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
||||
=========|_|==============|___/=/_/_/_/
|
||||
|
||||
:: Spring Boot :: (v4.1.0)
|
||||
|
||||
2026-08-04T15:49:27.744+08:00 INFO 10960 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : Starting FmsApiApplication using Java 21 with PID 10960 (D:\workspace\code\fms\fms-api\target\classes started by Administrator in D:\workspace\code\fms\fms-api)
|
||||
2026-08-04T15:49:27.753+08:00 INFO 10960 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2026-08-04T15:49:27.901+08:00 INFO 10960 --- [fms-api] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
|
||||
2026-08-04T15:49:27.902+08:00 INFO 10960 --- [fms-api] [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
|
||||
2026-08-04T15:49:29.777+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8088 (http)
|
||||
2026-08-04T15:49:29.805+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2026-08-04T15:49:29.807+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.22]
|
||||
2026-08-04T15:49:29.858+08:00 INFO 10960 --- [fms-api] [ restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 1954 ms
|
||||
2026-08-04T15:49:30.455+08:00 INFO 10960 --- [fms-api] [ restartedMain] c.g3soft.fmsapi.utils.IpRegionSearcher : ip2region Searcher 初始化完成,数据大小 11114380 字节
|
||||
2026-08-04T15:49:31.639+08:00 INFO 10960 --- [fms-api] [ restartedMain] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8088 (http) with context path '/api'
|
||||
2026-08-04T15:49:31.656+08:00 INFO 10960 --- [fms-api] [ restartedMain] cn.g3soft.fmsapi.FmsApiApplication : Started FmsApiApplication in 5.07 seconds (process running for 7.639)
|
||||
2026-08-04T15:50:46.035+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/api] : Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||
2026-08-04T15:50:46.038+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
|
||||
2026-08-04T15:50:46.039+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
|
||||
2026-08-04T15:50:46.425+08:00 INFO 10960 --- [fms-api] [nio-8088-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1,G3HD} inited
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] BUILD FAILURE
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] Total time: 01:37 h
|
||||
[INFO] Finished at: 2026-08-04T17:27:10+08:00
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:4.1.0:run (default-cli) on project fms-api: Process terminated with exit code: -1 -> [Help 1]
|
||||
[ERROR]
|
||||
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
|
||||
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
|
||||
[ERROR]
|
||||
[ERROR] For more information about the errors and possible solutions, please read the following articles:
|
||||
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
|
||||
@@ -11,22 +11,55 @@ SET XACT_ABORT ON;
|
||||
IF DB_NAME() <> N'FMS'
|
||||
THROW 53300, N'This migration can only run against the FMS database.', 1;
|
||||
|
||||
/* CUTOVER_INSTALLED_GUARD_START */
|
||||
IF OBJECT_ID(N'dbo.s_module_page', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
PRINT N'Module architecture already installed.';
|
||||
RETURN;
|
||||
END;
|
||||
/* CUTOVER_INSTALLED_GUARD_END */
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_module', N'U') IS NULL
|
||||
THROW 53301, N'dbo.s_module does not exist.', 1;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_module_profile', N'U') IS NOT NULL
|
||||
OR OBJECT_ID(N'dbo.s_function_node', N'U') IS NOT NULL
|
||||
OR OBJECT_ID(N'dbo.s_page_node', N'U') IS NOT NULL
|
||||
THROW 53302, N'Partial module architecture detected. Restore the database before retrying.', 1;
|
||||
DECLARE @HasPartialArchitecture bit = CASE WHEN
|
||||
OBJECT_ID(N'dbo.s_module_profile', N'U') IS NOT NULL
|
||||
OR OBJECT_ID(N'dbo.s_function_node', N'U') IS NOT NULL
|
||||
OR OBJECT_ID(N'dbo.s_page_node', N'U') IS NOT NULL
|
||||
THEN 1 ELSE 0 END;
|
||||
|
||||
IF @HasPartialArchitecture = 1 AND (
|
||||
COL_LENGTH(N'dbo.s_module', N'b_parent_id') IS NULL
|
||||
OR OBJECT_ID(N'dbo.s_module_field_list', N'U') IS NULL
|
||||
OR OBJECT_ID(N'dbo.s_module_field_edit', N'U') IS NULL
|
||||
OR OBJECT_ID(N'dbo.s_module_field_query', N'U') IS NULL
|
||||
OR OBJECT_ID(N'dbo.s_module_field_group', N'U') IS NULL
|
||||
OR OBJECT_ID(N'dbo.s_module_power', N'U') IS NULL
|
||||
OR OBJECT_ID(N'dbo.s_module_field', N'U') IS NULL
|
||||
OR OBJECT_ID(N'dbo.b_user_power', N'U') IS NULL
|
||||
OR OBJECT_ID(N'dbo.b_user_module', N'U') IS NULL
|
||||
)
|
||||
THROW 53302, N'Unsafe partial module architecture detected. Restore the database before retrying.', 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/*
|
||||
Earlier implementation work may have created these three derived tables while
|
||||
the original module tables were still authoritative. They are regenerated below
|
||||
from the validated source model, so remove them inside this transaction only.
|
||||
*/
|
||||
IF @HasPartialArchitecture = 1
|
||||
BEGIN
|
||||
IF OBJECT_ID(N'dbo.s_page_node', N'U') IS NOT NULL DROP TABLE dbo.s_page_node;
|
||||
IF OBJECT_ID(N'dbo.s_function_node', N'U') IS NOT NULL DROP TABLE dbo.s_function_node;
|
||||
IF OBJECT_ID(N'dbo.s_module_profile', N'U') IS NOT NULL DROP TABLE dbo.s_module_profile;
|
||||
END;
|
||||
GO
|
||||
|
||||
/* RETURN in the first batch means the architecture was already installed. */
|
||||
IF @@TRANCOUNT = 0
|
||||
RETURN;
|
||||
|
||||
/* Preserve the source tables only for the duration of this transaction. */
|
||||
DECLARE @LegacyTables TABLE (b_old sysname NOT NULL, b_legacy sysname NOT NULL, b_xh int NOT NULL);
|
||||
INSERT INTO @LegacyTables VALUES
|
||||
@@ -50,14 +83,16 @@ BEGIN TRANSACTION;
|
||||
|
||||
DECLARE @OldName sysname;
|
||||
DECLARE @LegacyName sysname;
|
||||
DECLARE @QualifiedOldName nvarchar(517);
|
||||
DECLARE table_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT b_old, b_legacy FROM @LegacyTables ORDER BY b_xh;
|
||||
OPEN table_cursor;
|
||||
FETCH NEXT FROM table_cursor INTO @OldName, @LegacyName;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
IF OBJECT_ID(N'dbo.' + @OldName, N'U') IS NOT NULL
|
||||
EXEC sys.sp_rename N'dbo.' + @OldName, @LegacyName, N'OBJECT';
|
||||
SET @QualifiedOldName = QUOTENAME(N'dbo') + N'.' + QUOTENAME(@OldName);
|
||||
IF OBJECT_ID(@QualifiedOldName, N'U') IS NOT NULL
|
||||
EXEC sys.sp_rename @QualifiedOldName, @LegacyName, N'OBJECT';
|
||||
FETCH NEXT FROM table_cursor INTO @OldName, @LegacyName;
|
||||
END;
|
||||
CLOSE table_cursor;
|
||||
@@ -66,6 +101,7 @@ BEGIN TRANSACTION;
|
||||
/* Constraint names are schema-wide and sp_rename does not change them with the table. */
|
||||
DECLARE @ConstraintName sysname;
|
||||
DECLARE @LegacyConstraintName sysname;
|
||||
DECLARE @QualifiedConstraintName nvarchar(517);
|
||||
DECLARE constraint_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT object_row.name,
|
||||
CONVERT(sysname, N'z_legacy_c_' + CONVERT(varchar(20), object_row.object_id))
|
||||
@@ -78,7 +114,8 @@ BEGIN TRANSACTION;
|
||||
FETCH NEXT FROM constraint_cursor INTO @ConstraintName, @LegacyConstraintName;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
EXEC sys.sp_rename N'dbo.' + QUOTENAME(@ConstraintName), @LegacyConstraintName, N'OBJECT';
|
||||
SET @QualifiedConstraintName = QUOTENAME(N'dbo') + N'.' + QUOTENAME(@ConstraintName);
|
||||
EXEC sys.sp_rename @QualifiedConstraintName, @LegacyConstraintName, N'OBJECT';
|
||||
FETCH NEXT FROM constraint_cursor INTO @ConstraintName, @LegacyConstraintName;
|
||||
END;
|
||||
CLOSE constraint_cursor;
|
||||
@@ -798,9 +835,10 @@ BEGIN TRANSACTION;
|
||||
|
||||
DECLARE @AdminId bigint;
|
||||
SELECT TOP (1) @AdminId = b_id FROM dbo.b_user
|
||||
WHERE b_account = N'g3soft' AND b_canuse = 1 ORDER BY b_id;
|
||||
WHERE b_canuse = 1
|
||||
ORDER BY CASE WHEN b_account = N'g3soft' THEN 0 ELSE 1 END, b_id;
|
||||
IF @AdminId IS NULL
|
||||
THROW 53305, N'Enabled g3soft administrator not found.', 1;
|
||||
THROW 53305, N'Enabled administrator user not found.', 1;
|
||||
|
||||
INSERT INTO dbo.b_role (b_id, b_code, b_name, b_i18n, b_is_super, b_canuse, b_xh)
|
||||
VALUES (@AdminId, 'system_admin', N'系统管理员', 'meta.role.' + CONVERT(varchar(30), @AdminId), 1, 1, 0);
|
||||
|
||||
@@ -2,8 +2,22 @@
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_module', N'U') IS NULL
|
||||
THROW 53400, N'Module architecture is not installed.', 1;
|
||||
IF EXISTS (
|
||||
SELECT required.name
|
||||
FROM (VALUES
|
||||
(N's_module'), (N's_module_page'), (N's_module_data'), (N's_module_report'),
|
||||
(N's_module_dashboard'), (N's_module_workflow'), (N's_function_node'),
|
||||
(N's_page_node'), (N's_module_profile'), (N's_module_field'),
|
||||
(N's_module_field_list'), (N's_module_field_form'), (N's_module_field_query'),
|
||||
(N's_module_action'), (N'b_role'), (N'b_user_role'),
|
||||
(N'b_role_function_node'), (N'b_role_page_node'), (N'b_role_action'),
|
||||
(N'b_user_function_node'), (N'b_user_page_node'), (N'b_user_action')
|
||||
) AS required(name)
|
||||
LEFT JOIN sys.tables AS actual
|
||||
ON actual.name = required.name AND actual.schema_id = SCHEMA_ID(N'dbo')
|
||||
WHERE actual.object_id IS NULL
|
||||
)
|
||||
THROW 53400, N'Module architecture is incomplete. Run migration 033 first.', 1;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW dbo.v_s_function_node
|
||||
|
||||
@@ -9,7 +9,20 @@ Run against a backed-up `FMS` database in this order:
|
||||
3. `verify_module_architecture.sql`
|
||||
|
||||
`033` is a one-time transaction: it migrates valid metadata and removes the old metadata tables only after conversion checks pass. It intentionally does not create compatibility views or dual writes.
|
||||
If the earlier intermediate implementation created only `s_module_profile`, `s_function_node`, or `s_page_node`, `033` verifies that the complete source model still exists and regenerates those derived tables inside the same transaction. Other partial target states are rejected.
|
||||
|
||||
`034` uses SQL Server `GO` batch separators because each `CREATE OR ALTER VIEW/TRIGGER` must start its batch. Use SSMS, Azure Data Studio, `sqlcmd`, or another runner that understands `GO`.
|
||||
`033` and `034` use SQL Server `GO` batch separators. Use SSMS, Azure Data Studio, `sqlcmd`, or another runner that understands `GO`.
|
||||
|
||||
`verify_module_architecture.sql` is a target-schema verification script. It does not modify application tables.
|
||||
|
||||
To check and conditionally apply `034` with the repository JDBC driver on Windows:
|
||||
|
||||
```powershell
|
||||
$jdk = "D:\devtool\jdk\jdk-21\bin"
|
||||
$driver = "tools\migration\mssql-jdbc-13.4.0.jre11.jar"
|
||||
& "$jdk\javac.exe" -encoding UTF-8 -cp $driver -d $env:TEMP config\migrations\tools\ApplyModuleRuntimeViews.java
|
||||
& "$jdk\java.exe" -cp "$env:TEMP;$driver" ApplyModuleRuntimeViews
|
||||
```
|
||||
|
||||
The tool refuses non-`FMS` catalogs and executes `034` only when a required runtime view is missing.
|
||||
For a database in the validated intermediate state, run `ApplyModuleRuntimeViews --dry-run-cutover` first. After verifying a database backup, `ApplyModuleRuntimeViews --apply-cutover` executes `033`, then conditionally executes `034` and verifies all required runtime views.
|
||||
@@ -0,0 +1,302 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user