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

View File
Whitespace-only changes.
+65
View File
@@ -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
+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);
}
}