values, String key, Object value) {
private static String version() {
Package pkg = FlydbCli.class.getPackage();
String implementation = pkg == null ? null : pkg.getImplementationVersion();
- return implementation != null ? implementation : "0.3.7";
+ return implementation != null ? implementation : "0.3.8";
}
private static Path detectInstallDirectory() {
diff --git a/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java b/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java
index afa9487..7abb6bb 100644
--- a/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java
+++ b/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java
@@ -44,7 +44,7 @@ void versionCommandReturnsSuccess() {
int exitCode = cli.execute("version");
assertThat(exitCode).isZero();
- assertThat(standardOutput.toString()).contains("flydb 0.3.7");
+ assertThat(standardOutput.toString()).contains("flydb 0.3.8");
assertThat(errorOutput.toString()).isEmpty();
}
@@ -63,7 +63,7 @@ void jsonVersionEmitsSingleLineEnvelope() {
assertThat(exitCode).isZero();
assertThat(standardOutput.toString())
.isEqualTo("{\"protocolVersion\":1,\"command\":\"version\","
- + "\"status\":\"success\",\"exitCode\":0,\"version\":\"0.3.7\"}\n");
+ + "\"status\":\"success\",\"exitCode\":0,\"version\":\"0.3.8\"}\n");
assertThat(errorOutput.toString()).isEmpty();
}
diff --git a/flydb-core/src/main/java/com/flydb/core/dialect/DmDatabase.java b/flydb-core/src/main/java/com/flydb/core/dialect/DmDatabase.java
index 3dacf64..130020f 100644
--- a/flydb-core/src/main/java/com/flydb/core/dialect/DmDatabase.java
+++ b/flydb-core/src/main/java/com/flydb/core/dialect/DmDatabase.java
@@ -1,6 +1,7 @@
package com.flydb.core.dialect;
import java.sql.Connection;
+import java.sql.SQLException;
import com.flydb.core.api.FlydbConfiguration;
import com.flydb.core.history.SchemaHistory;
@@ -23,13 +24,17 @@ public boolean caseSensitive() {
@Override
public SchemaHistoryDdl schemaHistoryDdl() {
final SchemaHistoryDdl delegate = super.schemaHistoryDdl();
- if (!caseSensitive) {
- return delegate;
- }
return new SchemaHistoryDdl() {
@Override
public String tableName(String rawTableName) {
- return quote(rawTableName);
+ return caseSensitive ? quote(rawTableName) : delegate.tableName(rawTableName);
+ }
+
+ @Override
+ public boolean isTableAlreadyExists(SQLException exception) {
+ // DM8 8.1.3.62:对象已存在为 -2124,SQLState 22000 还包含其他错误。
+ return exception.getErrorCode() == -2124
+ || delegate.isTableAlreadyExists(exception);
}
@Override
@@ -41,6 +46,11 @@ public String createTableSql(String tableName) {
public String createLockTableSql(String lockTableName) {
return delegate.createLockTableSql(tableName(lockTableName));
}
+
+ @Override
+ public String dropTableSql(String tableName) {
+ return delegate.dropTableSql(tableName);
+ }
};
}
diff --git a/flydb-core/src/main/java/com/flydb/core/history/SchemaHistory.java b/flydb-core/src/main/java/com/flydb/core/history/SchemaHistory.java
index ecd32c7..b68caea 100644
--- a/flydb-core/src/main/java/com/flydb/core/history/SchemaHistory.java
+++ b/flydb-core/src/main/java/com/flydb/core/history/SchemaHistory.java
@@ -37,7 +37,7 @@ public SchemaHistory(String table, SchemaHistoryDdl ddl, Connection connection)
* 幂等创建历史表 + 锁表。
*
* 建表动作发生在获取锁之前(锁表本身需要先存在),因此建表必须自身幂等且容忍竞态。
- * 调用者应确保在获取锁之后调用此方法。
+ * 调用者应在获取锁之前调用此方法。
*/
public void ensureExists() {
executeCreateTable(ddl.createTableSql(table));
@@ -85,9 +85,8 @@ private void executeCreateTable(String sql) {
stmt = connection.createStatement();
stmt.execute(sql);
} catch (SQLException e) {
- // Oracle 系无 IF NOT EXISTS:表已存在错误码(ORA-00955)→ 幂等忽略
- // 其他系直接用 IF NOT EXISTS 无此问题
- if (e.getErrorCode() != 955) { // ORA-00955: name is already used by an existing object
+ // 由方言识别产品的已存在错误,其他建表失败必须继续上报。
+ if (!ddl.isTableAlreadyExists(e)) {
throw new FlydbException(ErrorCode.MIGRATION_EXECUTION_FAILED,
"建表失败: " + e.getMessage(), e);
}
diff --git a/flydb-core/src/main/java/com/flydb/core/history/SchemaHistoryDdl.java b/flydb-core/src/main/java/com/flydb/core/history/SchemaHistoryDdl.java
index 8700ff2..a71c5d1 100644
--- a/flydb-core/src/main/java/com/flydb/core/history/SchemaHistoryDdl.java
+++ b/flydb-core/src/main/java/com/flydb/core/history/SchemaHistoryDdl.java
@@ -1,12 +1,14 @@
package com.flydb.core.history;
+import java.sql.SQLException;
+
/**
* 历史表 DDL 模板(设计 03 §5)。
*
*
封装各方言家族的历史表与锁表建表 DDL 差异。由 {@link SchemaHistory#ensureExists()} 调用。
*
*
PG 系/MySQL 系:直接 {@code CREATE TABLE IF NOT EXISTS}(幂等)。
- * Oracle 系:先查系统目录判断存在性,不存在则建表,并捕获"表已存在"错误码兜底。
+ * Oracle 系:尝试建表,仅忽略方言确认的"表已存在"错误,兼容并发初始化。
*/
public interface SchemaHistoryDdl {
@@ -15,6 +17,14 @@ default String tableName(String rawTableName) {
return rawTableName;
}
+ /**
+ * 判定建表失败是否仅因对象已存在。默认保留 Oracle 955 的既有行为;
+ * 厂商方言可补充自身错误码,不能按宽泛的 SQLState 忽略其他建表错误。
+ */
+ default boolean isTableAlreadyExists(SQLException exception) {
+ return exception.getErrorCode() == 955;
+ }
+
/**
* 返回历史表建表 SQL(含家族特定的列类型与默认值)。
*
@@ -113,7 +123,7 @@ public String createLockTableSql(String lockTableName) {
final class OracleSchemaHistoryDdl implements SchemaHistoryDdl {
@Override
public String createTableSql(String tableName) {
- // Oracle 系无 IF NOT EXISTS,使用 "查系统目录后建表" 的幂等策略
+ // Oracle 系无 IF NOT EXISTS,使用方言错误码判定保证幂等与竞态安全
// Oracle 列定义要求 DEFAULT 子句在 NOT NULL 之前(NOT NULL DEFAULT 顺序报 ORA-00907)
return "CREATE TABLE " + tableName + " (\n"
+ " installed_rank INT NOT NULL,\n"
diff --git a/flydb-core/src/test/java/com/flydb/core/history/SchemaHistoryTest.java b/flydb-core/src/test/java/com/flydb/core/history/SchemaHistoryTest.java
index c15b0a4..9b8bc7c 100644
--- a/flydb-core/src/test/java/com/flydb/core/history/SchemaHistoryTest.java
+++ b/flydb-core/src/test/java/com/flydb/core/history/SchemaHistoryTest.java
@@ -1,7 +1,9 @@
package com.flydb.core.history;
+import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
+import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
@@ -9,6 +11,7 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import com.flydb.core.dialect.DmDatabase;
import com.flydb.core.migration.AppliedMigration;
import com.flydb.core.migration.MigrationType;
import com.flydb.core.migration.MigrationVersion;
@@ -55,6 +58,88 @@ void mysqlStyleEnsureExists() throws Exception {
&& sql.contains("IF NOT EXISTS")
&& sql.contains("TINYINT(1)"));
}
+
+ @Test
+ @DisplayName("达梦大小写不敏感实例重复初始化历史表和锁表保持幂等")
+ void dmCaseInsensitiveExistingTablesAreIdempotent() {
+ assertDmExistingTablesAreIdempotent(false);
+ }
+
+ @Test
+ @DisplayName("达梦大小写敏感实例重复初始化带引号的历史表和锁表保持幂等")
+ void dmCaseSensitiveExistingTablesAreIdempotent() {
+ assertDmExistingTablesAreIdempotent(true);
+ }
+
+ @Test
+ @DisplayName("达梦不会忽略同为 SQLState 22000 的其他建表错误")
+ void dmOtherCreateErrorsAreNotSwallowed() {
+ SQLException failure = new SQLException("syntax error", "22000", -2007);
+ Connection connection = failingCreateConnection(new ArrayList(), failure);
+ SchemaHistory history = new SchemaHistory(TABLE,
+ new DmDatabase(connection, false).schemaHistoryDdl(), connection);
+
+ assertThatThrownBy(history::ensureExists).hasCause(failure);
+ }
+
+ @Test
+ @DisplayName("Oracle 已存在错误 955 仍保持幂等")
+ void oracleExistingTablesRemainIdempotent() {
+ List attempted = new ArrayList();
+ Connection connection = failingCreateConnection(attempted,
+ new SQLException("name is already used", "42000", 955));
+ SchemaHistory history = new SchemaHistory(TABLE, SchemaHistoryDdl.oracle(), connection);
+
+ history.ensureExists();
+ history.ensureExists();
+
+ assertThat(attempted).hasSize(4);
+ }
+
+ @Test
+ @DisplayName("达梦已存在错误码不影响 Oracle 方言的错误处理")
+ void oracleDoesNotSwallowDmErrorCode() {
+ SQLException failure = new SQLException("unrelated error", "22000", -2124);
+ Connection connection = failingCreateConnection(new ArrayList(), failure);
+ SchemaHistory history = new SchemaHistory(TABLE, SchemaHistoryDdl.oracle(), connection);
+
+ assertThatThrownBy(history::ensureExists).hasCause(failure);
+ }
+
+ private void assertDmExistingTablesAreIdempotent(boolean caseSensitive) {
+ List attempted = new ArrayList();
+ Connection connection = failingCreateConnection(attempted,
+ new SQLException("object already exists", "22000", -2124));
+ SchemaHistory history = new SchemaHistory(TABLE,
+ new DmDatabase(connection, caseSensitive).schemaHistoryDdl(), connection);
+
+ history.ensureExists();
+ history.ensureExists();
+
+ String historyName = caseSensitive ? "\"" + TABLE + "\"" : TABLE;
+ String lockName = caseSensitive ? "\"flydb_schema_lock\"" : "flydb_schema_lock";
+ assertThat(attempted).hasSize(4);
+ assertThat(attempted.get(0)).startsWith("CREATE TABLE " + historyName + " (");
+ assertThat(attempted.get(1)).startsWith("CREATE TABLE " + lockName + " (");
+ }
+ }
+
+ private static Connection failingCreateConnection(List attempted,
+ SQLException failure) {
+ return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(),
+ new Class>[]{Connection.class}, (proxy, method, args) -> {
+ if ("createStatement".equals(method.getName())) {
+ return Proxy.newProxyInstance(Statement.class.getClassLoader(),
+ new Class>[]{Statement.class}, (statement, call, values) -> {
+ if ("execute".equals(call.getName())) {
+ attempted.add((String) values[0]);
+ throw failure;
+ }
+ return JdbcFakes.defaultValue(call.getReturnType());
+ });
+ }
+ return JdbcFakes.defaultValue(method.getReturnType());
+ });
}
@Nested
@@ -145,4 +230,4 @@ void lockTableDdl() {
.contains("INT PRIMARY KEY");
}
}
-}
\ No newline at end of file
+}
diff --git a/flydb-skills/plugin.json b/flydb-skills/plugin.json
index 1eaf4eb..9673b51 100644
--- a/flydb-skills/plugin.json
+++ b/flydb-skills/plugin.json
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "flydb",
- "version": "0.3.7",
+ "version": "0.3.8",
"description": "面向 JDBC 数据库的 Schema 版本化迁移工具:flydb-cli 指导 Agent 使用本机 GUI 并安全执行迁移;flydb-analysis 提供方言预检、结构漂移、对象依赖、应用引用与变更影响分析。",
"homepage": "https://github.com/zzxCoding/Flydb",
"repository": "https://github.com/zzxCoding/Flydb",
diff --git a/flydb-web/frontend/package-lock.json b/flydb-web/frontend/package-lock.json
index dcd7aa2..b34e69d 100644
--- a/flydb-web/frontend/package-lock.json
+++ b/flydb-web/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@flydb/workbench",
- "version": "0.3.7",
+ "version": "0.3.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@flydb/workbench",
- "version": "0.3.7",
+ "version": "0.3.8",
"dependencies": {
"@codemirror/autocomplete": "6.20.3",
"@codemirror/commands": "6.11.0",
diff --git a/flydb-web/frontend/package.json b/flydb-web/frontend/package.json
index 34bd45e..8fe14a3 100644
--- a/flydb-web/frontend/package.json
+++ b/flydb-web/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "@flydb/workbench",
"private": true,
- "version": "0.3.7",
+ "version": "0.3.8",
"type": "module",
"engines": {
"node": ">=22.12.0"
diff --git a/pom.xml b/pom.xml
index 7f0fc0d..807b517 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,7 +55,7 @@
- 0.3.7
+ 0.3.8
UTF-8
UTF-8