20260804173126

This commit is contained in:
oneao committed 2026-08-04 17:31:27 +08:00
1 parent e17bbe9db3
commit 75b1ccaa6d
36 files changed
+3521 -718

No files matched your search

@@ -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
+14 -1
View File
@@ -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);
}
}