diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8daa7..25da3b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ Flydb 的重要变更记录在本文件中。版本遵循语义化版本;正 ## [Unreleased] +## [0.3.8] - 2026-10-21 + +### 修复 + +- 修复达梦 DM8 重复执行迁移时因历史表或锁表已存在而报 `FLYDB-2010` 的问题。DM8 8.1.3.62 返回的“对象已存在”错误码为 `-2124`,此前仅识别 Oracle `955`;现在由 `SchemaHistoryDdl.isTableAlreadyExists(SQLException)` 交给方言判定,达梦额外识别该错误码,其他建表错误仍阻断迁移。 +- 历史表 DDL 新增 Java 8 默认方法,保留既有 SPI 的错误处理行为;CLI 命令、配置和 JSON 协议不变,不需要修改已有迁移脚本或执行 `repair`。 + +### 验证与文档 + +- 在授权本地 DM8 8.1.3.62、大小写不敏感模式完成连接、validate、dry-run、中文数据与 PL/SQL 块迁移、视图查询和重复迁移验证;记录实测边界与旧版本复现,不扩大为厂商认证或并发锁竞争验证。 +- 新增达梦历史表/锁表重复初始化回归,覆盖大小写敏感与不敏感的离线分支、其他 SQLState 相同的建表错误不被忽略,以及 Oracle 原有行为;同步达梦指南、设计说明和中英文数据库验证矩阵。 + +### 发行说明 + +- 产品、CLI、随包插件与工作台版本同步为 `0.3.8`;MCP Adapter 独立版本仍为 `0.1.0`。 +- 纳入上个正式版本以来 main 已合并的依赖更新,包括 Jackson Databind `2.21.7`、MCP SDK `1.31.0` 和 Vite `8.3.5`,以及对应锁文件更新。 + ## [0.3.7] - 2026-09-08 本次重点完善本机工作台的日常使用:大变更预览更流畅,多配置更易整理,问题上下文可直接交给 Agent;修复大记录读取与迁移状态显示问题。 diff --git a/README.en.md b/README.en.md index e63f3d5..300a042 100644 --- a/README.en.md +++ b/README.en.md @@ -37,7 +37,7 @@ MySQL, PostgreSQL, Oracle and Chinese databases including OceanBase, TiDB, DM8, Download the ZIP from [Releases](https://github.com/zzxCoding/Flydb/releases/latest), extract it and run: ```bash -cd flydb-cli-0.3.7 +cd flydb-cli-0.3.8 bin/flydb web ``` @@ -87,7 +87,7 @@ Flydb manages migration workflow and database dialect behavior. It does not tran | MySQL | Yes | Automated contract tests; CLI distribution end-to-end verification | | PostgreSQL | Yes | Automated contract tests | | Oracle | Yes | Automated contract tests; end-to-end validation (validate / clean / migrate) completed on a licensed real instance | -| DM8 (Dameng) | Yes | Dialect and driver-metadata contract tests; real-environment certification pending | +| DM8 (Dameng) | Yes | End-to-end verified on an authorized DM8 8.1.3.62 instance in case-insensitive mode; 0.3.8 fixes repeated migrations ([test record](./docs/verification/dm8-20261021.md)) | | KingbaseES | Yes | Dialect and driver-metadata contract tests; real-environment certification pending | | openGauss | Yes | Dialect and driver-metadata contract tests; real-environment certification pending | | OceanBase | Oracle/MySQL family reuse | Oracle tenant end-to-end validated on a licensed real instance; MySQL tenant under lightweight compatibility tests | @@ -143,7 +143,7 @@ Plain Java applications depend on `flydb-core`: io.github.zzxcoding flydb-core - 0.3.7 + 0.3.8 ``` @@ -154,13 +154,13 @@ Spring Boot applications pick the matching starter; it runs `migrate` during con io.github.zzxcoding flydb-spring-boot-3-starter - 0.3.7 + 0.3.8 io.github.zzxcoding flydb-spring-boot-2-starter - 0.3.7 + 0.3.8 ``` @@ -204,7 +204,7 @@ The full reactor, including the Boot 3 modules, is built with Java 17; the Boot ./mvnw verify ``` -The CLI distribution is generated at `flydb-cli/target/flydb-cli-0.3.7.zip`. The core module enforces an 80% JaCoCo line-coverage gate and zero non-test runtime dependencies via Maven Enforcer. +The CLI distribution is generated at `flydb-cli/target/flydb-cli-0.3.8.zip`. The core module enforces an 80% JaCoCo line-coverage gate and zero non-test runtime dependencies via Maven Enforcer. Database integration contracts are skipped by default. Set `-Pmysql`/`-Ppostgresql` and `-Dflydb.integration.database=` explicitly to start temporary databases and run the selected tests. The full matrix runs in `.github/workflows/ci.yml`. diff --git a/README.md b/README.md index 5fdcb3c..3cdcfcd 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Flydb 是给开发、运维和实施人员使用的数据库版本迁移工具 从 [Releases](https://github.com/zzxCoding/Flydb/releases/latest) 下载 ZIP,解压后运行: ```bash -cd flydb-cli-0.3.7 +cd flydb-cli-0.3.8 bin/flydb web ``` @@ -87,7 +87,7 @@ Flydb 管理迁移流程与数据库方言行为,不会把任意厂商 SQL 自 | MySQL | 是 | 自动化兼容测试;CLI 发行包端到端验证 | | PostgreSQL | 是 | 自动化兼容测试 | | Oracle | 是 | 自动化契约测试;已在授权真实实例完成 validate、clean、migrate 端到端验证 | -| 达梦 DM8 | 是 | 方言与驱动元数据契约测试;真实环境认证待补 | +| 达梦 DM8 | 是 | DM8 8.1.3.62 授权实例端到端验证(大小写不敏感);0.3.8 修复重复迁移,见[实测记录](./docs/verification/dm8-20261021.md) | | 人大金仓 KingbaseES | 是 | 方言与驱动元数据契约测试;真实环境认证待补 | | openGauss | 是 | 方言与驱动元数据契约测试;真实环境认证待补 | | OceanBase | 复用 Oracle/MySQL 家族 | Oracle 租户已在授权真实实例完成端到端验证;MySQL 租户为轻量兼容测试 | @@ -143,7 +143,7 @@ flydb.migrate(); io.github.zzxcoding flydb-core - 0.3.7 + 0.3.8 ``` @@ -154,13 +154,13 @@ Spring Boot 应用选择对应 starter,容器初始化期间执行 `migrate` io.github.zzxcoding flydb-spring-boot-3-starter - 0.3.7 + 0.3.8 io.github.zzxcoding flydb-spring-boot-2-starter - 0.3.7 + 0.3.8 ``` @@ -204,7 +204,7 @@ bin/flydb clean --clean-disabled=false --force # clean 默认禁用;非交 ./mvnw verify ``` -CLI 构建产物位于 `flydb-cli/target/flydb-cli-0.3.7.zip`。core 的 JaCoCo 行覆盖率门禁为 80%,并由 Maven Enforcer 保证零非测试运行时依赖。 +CLI 构建产物位于 `flydb-cli/target/flydb-cli-0.3.8.zip`。core 的 JaCoCo 行覆盖率门禁为 80%,并由 Maven Enforcer 保证零非测试运行时依赖。 数据库集成契约默认跳过;显式设置 `-Pmysql`/`-Ppostgresql` 与 `-Dflydb.integration.database=` 后,才会启动临时数据库执行对应测试。完整矩阵由 `.github/workflows/ci.yml` 执行。 diff --git a/docs/design/03-dialects.md b/docs/design/03-dialects.md index 4686215..ff26aee 100644 --- a/docs/design/03-dialects.md +++ b/docs/design/03-dialects.md @@ -92,7 +92,7 @@ public interface Database extends AutoCloseable { | 标识符引用 | 双引号(引用即大小写敏感) | | 语句切分 | 识别 PL/SQL 块(`CREATE [OR REPLACE] [EDITIONABLE\|NONEDITIONABLE] PROCEDURE/FUNCTION/TRIGGER/PACKAGE/TYPE`、裸 `DECLARE`/`BEGIN`),块终止符为独占一行的 `/`;`EDITIONABLE VIEW/SYNONYM` 仍是普通单语句 | | 锁 | 通用锁表方案(家族默认);专用锁连接使用包含当前 schema 的全限定锁表名 | -| 历史表 DDL | 无 `IF NOT EXISTS` → "先查系统目录后建 + 建表异常兜底"的幂等策略;`success` 用 `NUMBER(1)`;`installed_on TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL`——Oracle 列定义要求 `DEFAULT` 在 `NOT NULL` 之前,反序报 `ORA-00907` | +| 历史表 DDL | 无 `IF NOT EXISTS` → 尝试建表,仅忽略方言确认的“对象已存在”错误,以兼容重复调用与并发初始化;`success` 用 `NUMBER(1)`;`installed_on TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL`——Oracle 列定义要求 `DEFAULT` 在 `NOT NULL` 之前,反序报 `ORA-00907` | | clean | 专用 `OracleCleanStrategy`:序列按待清理 schema 查 `all_sequences`(跳过 `ISEQ$$_`/`BIN$`)——JDBC `getTables` 不返回序列且无 `information_schema`;`DROP TABLE ... PURGE` 并收尾 `PURGE RECYCLEBIN`(失败降级警告),避免回收站残留的表/LOB 段/索引占名导致 `CREATE` 撞名;OceanBase 直接返回 `-4007`,或以 vendor code `600` 的 `ORA-00600 arguments` 包装 `-4007` 时,等待 `all_tab_columns` 列数稳定后有限重试,并跳过 `_...hidden...` DDL 中间表;记账表排除忽略大小写(非引用名大写存储) | | currentSchema | 查询会话当前 schema(达梦:`SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM dual`,实施时确认) | @@ -119,9 +119,11 @@ public interface Database extends AutoCloseable { `SchemaHistoryDdl` 返回两组 DDL(历史表 + 锁表,见 [04 §2.2](04-parser-lock-tx.md)),由 `SchemaHistory.ensureExists()` 调用: 1. PG 系/MySQL 系:直接 `CREATE TABLE IF NOT EXISTS`(幂等)。 -2. Oracle 系:先查系统目录判断存在性;不存在则建表,并捕获"表已存在"错误码兜底(两个进程同时初始化的竞态窗口)。 +2. Oracle 系:尝试建表,由 `SchemaHistoryDdl.isTableAlreadyExists(SQLException)` 判定异常是否可以忽略。默认保留 Oracle `955`;达梦方言额外识别 DM8 8.1.3.62 实测的 `-2124`。不能按通用 SQLState(例如 DM 的 `22000`)忽略语法或权限错误。厂商差异留在方言,历史仓储不维护产品错误码列表。 3. 建表动作发生在获取锁**之前**(锁表本身需要先存在),因此建表必须自身幂等且容忍竞态——这是流程顺序上的硬约束(见 [05 §1](05-commands.md) 时序第 4~5 步)。 +2026-10-21 实例验证确认:原实现直接尝试建表,仅识别 Oracle `955`,与旧文档的“先查系统目录”描述不符,达梦重复迁移因此失败。这里明确采用现有的建表加方言异常判定策略;目录预查不能替代并发场景中的异常兜底。 + ## 6. 扩展新方言的成本模型(二期验收标准) 新增一个 PG 系国产库(如瀚高)的预期工作量,用于验证本设计的扩展性: diff --git a/docs/getting-started/ci-integration.md b/docs/getting-started/ci-integration.md index da59657..17beeb6 100644 --- a/docs/getting-started/ci-integration.md +++ b/docs/getting-started/ci-integration.md @@ -22,7 +22,7 @@ jobs: migrate: runs-on: ubuntu-latest env: - FLYDB_HOME: ${{ runner.temp }}/flydb-cli-0.3.7 # ZIP 内的版本化根目录 + FLYDB_HOME: ${{ runner.temp }}/flydb-cli-0.3.8 # ZIP 内的版本化根目录 CONF: deploy/flydb.mysql.uat.conf steps: - uses: actions/checkout@v4 @@ -30,7 +30,7 @@ jobs: - name: 安装 Flydb CLI(版本随仓库锁定) run: | curl -fsSL -o flydb.zip \ - "https://github.com/zzxCoding/Flydb/releases/download/v0.3.7/flydb-cli-0.3.7.zip" + "https://github.com/zzxCoding/Flydb/releases/download/v0.3.8/flydb-cli-0.3.8.zip" unzip -q flydb.zip -d "$RUNNER_TEMP" "$FLYDB_HOME/bin/flydb" version diff --git a/docs/getting-started/dm8.md b/docs/getting-started/dm8.md index af4f489..87d38b5 100644 --- a/docs/getting-started/dm8.md +++ b/docs/getting-started/dm8.md @@ -2,18 +2,32 @@ ## 支持边界 -达梦 DM8 使用 Oracle 家族的 SQL 切分、历史表和锁表语义。当前已完成方言、元数据和驱动契约设计;真实实例验证需要授权环境和厂商驱动,公共 CI 不捆绑 DM 驱动。 +达梦 DM8 使用 Oracle 家族的 SQL 切分、历史表和锁表语义。已在授权本地 DM8 8.1.3.62 实例、大小写不敏感模式完成 CLI 连接、`validate`、`--dry-run migrate`、首次迁移和重复迁移验证。公共 CI 不捆绑 DM 驱动;该结果不代表厂商认证或其他版本、大小写模式已验证。完整环境、命令与结果见 [2026-10-21 实测记录](../verification/dm8-20261021.md)。 + +0.3.8 修复了历史表重复初始化缺陷:DM8 返回“对象已存在”错误码 `-2124`,原 CLI 0.3.7 仅识别 Oracle `955`,导致第二次 `migrate` 报 `FLYDB-2010`。首次复验使用本地源码修复包 `0.3.7-local.dm8.20261021`,详细记录保留了该验证包的身份;正式版本获取状态以 [GitHub Releases](https://github.com/zzxCoding/Flydb/releases) 为准。 ## 驱动与连接 从达梦安装介质或企业制品库获取 JDBC 驱动,使用 `jdbc:dm://` URL,并显式指定 `dm`: ```bash -bin/flydb init --url 'jdbc:dm://127.0.0.1:5236/demo' \ +bin/flydb init --url 'jdbc:dm://127.0.0.1:5236' \ --user SYSDBA --database-type dm --yes ``` -驱动放在 CLI 的 `drivers/` 目录;应用依赖由调用方按厂商许可管理。真实契约环境变量为 `FLYDB_TEST_DM_URL`、`FLYDB_TEST_DM_USER`、`FLYDB_TEST_DM_PASSWORD`。 +驱动放在 CLI 的 `drivers/` 目录;应用依赖由调用方按厂商许可管理。本次复用本地已有的 `DmJdbcDriver18` 8.1.3.62,未联网下载或分发驱动。达梦驱动没有内置固定 Maven 坐标,只有本地 Maven 缓存时仍需显式配置完整 `flydb.driver-coordinate`,或将现有 JAR 放入 `drivers/`。 + +真实只读契约环境变量为 `FLYDB_TEST_DM_URL`、`FLYDB_TEST_DM_USER`、`FLYDB_TEST_DM_PASSWORD`;将驱动加入测试运行时 classpath 后可执行: + +```bash +# 密码预先从受控环境注入;JDK 17 构建,不会自动执行迁移。 +./mvnw -B -pl flydb-integration-tests -am \ + -Dtest=LicensedDatabaseContractTest#dmRealInstanceContract \ + -Dsurefire.failIfNoSpecifiedTests=false \ + -Dmaven.test.additionalClasspath=/path/to/DmJdbcDriver18.jar test +``` + +必须核对该用例实际执行且 `Skipped: 0`;只看到 `BUILD SUCCESS` 不等于已连接真库。需要写库验证时,仍应先完成 `validate`、dry-run 和目标范围授权。 ## 已知限制 diff --git a/docs/getting-started/mcp-adapter.md b/docs/getting-started/mcp-adapter.md index 8e21276..c1116f1 100644 --- a/docs/getting-started/mcp-adapter.md +++ b/docs/getting-started/mcp-adapter.md @@ -52,7 +52,7 @@ npx flydb-mcp # 一次性启动(发布后可用;registry 名称以发 "command": "npx", "args": ["flydb-mcp"], "env": { - "FLYDB_CLI": "/opt/flydb-cli-0.3.7/bin/flydb", + "FLYDB_CLI": "/opt/flydb-cli-0.3.8/bin/flydb", "FLYDB_PASSWORD": "由宿主 secret 注入" } } diff --git a/docs/reference/compatibility.md b/docs/reference/compatibility.md index 3726ada..7509c42 100644 --- a/docs/reference/compatibility.md +++ b/docs/reference/compatibility.md @@ -33,7 +33,7 @@ Maven 坐标 groupId 统一为 `io.github.zzxcoding`(Central Portal 经 GitHub | MySQL | `mysql` | `com.mysql:mysql-connector-j` | `com.mysql.cj.jdbc.Driver` | 官方声明支持 JRE 8+ | 自动化兼容测试;CLI 发行包端到端验证 | | PostgreSQL | `postgresql` | `org.postgresql:postgresql` 42.x | `org.postgresql.Driver` | 官方声明支持 JRE 8+ | 自动化兼容测试 | | Oracle | `oracle` | Oracle 制品库 `ojdbc` | `oracle.jdbc.OracleDriver` | 见厂商文档 | 自动化契约测试;已在授权真实实例完成 validate、clean、migrate 端到端验证 | -| 达梦 DM8 | `dm` | 达梦安装介质或企业制品库 | `dm.jdbc.driver.DmDriver` | 见厂商文档 | 方言与驱动元数据契约测试;真实环境认证待补 | +| 达梦 DM8 | `dm` | 达梦安装介质或企业制品库 | `dm.jdbc.driver.DmDriver` | 见厂商文档 | DM8 8.1.3.62 授权实例端到端验证(大小写不敏感);0.3.8 修复重复迁移,见[实测记录](../verification/dm8-20261021.md) | | 人大金仓 KingbaseES | `kingbasees` | 人大金仓交付介质(`cn.com.kingbase:kingbase8`,选不带 `.jre` 后缀的 JDK8+ 版本) | `com.kingbase8.Driver` | 见厂商文档 | 方言与驱动元数据契约测试;真实环境认证待补 | | openGauss | `opengauss` | `org.opengauss:opengauss-jdbc` | `org.opengauss.Driver` | 见厂商文档 | 方言与驱动元数据契约测试;真实环境认证待补 | | OceanBase | `oceanbase` | `com.oceanbase:oceanbase-client` | `com.oceanbase.jdbc.Driver` | 官方声明基于 Java 8 开发 | Oracle 租户已在授权真实实例完成端到端验证;MySQL 租户为轻量兼容测试 | diff --git a/docs/verification/dm8-20261021.md b/docs/verification/dm8-20261021.md new file mode 100644 index 0000000..b997219 --- /dev/null +++ b/docs/verification/dm8-20261021.md @@ -0,0 +1,135 @@ +# 达梦 DM8 实例验证记录(2026-10-21) + +## 结论与适用范围 + +已在用户提供并授权的本地 DM8 8.1.3.62 实例完成只读契约、CLI 校验、预演、首次迁移和重复迁移验证。首次迁移使用原 CLI 0.3.7;第二次运行发现历史表重复初始化缺陷。源码修复后,本地发行包 `0.3.7-local.dm8.20261021` 连续两次零变更迁移成功。 + +该记录仅覆盖本次版本、驱动及大小写不敏感模式,不代表厂商认证。修复纳入 0.3.8;本页保留最初本地验证包的身份,正式版本获取状态以 [GitHub Releases](https://github.com/zzxCoding/Flydb/releases) 为准。本次未执行 `clean`、`repair`、`baseline`、`undo`,未做并发锁竞争、失败 DDL 恢复、大小写敏感或 `compatibleMode=oracle` 模式的真库验证。迁移能够获取并释放锁,但不能据此宣称并发互斥已实测。 + +## 实测环境与产物身份 + +| 项目 | 实测值 | +|---|---| +| 源码基线 | `7fc4da9f507bbedc4d95389dc8a7833a28e4899f`,本次修复为其上的工作区变更 | +| 容器 | `dm8`,镜像 `qinchz/dm8-arm64:latest`;镜像标签不作为固定镜像身份 | +| JDBC 目标 | `jdbc:dm://127.0.0.1:5236`,本地测试环境 | +| 当前 schema | `SYSDBA`;密码仅通过环境注入,未写入配置或 SQL | +| JDBC 产品/版本 | `DM DBMS` / `8.1.3.62` | +| JDBC 驱动 | 本地已有 `DmJdbcDriver18` / `8.1.3.62`,未联网下载或提交 | +| 大小写模式 | `SELECT SF_GET_CASE_SENSITIVE_FLAG() FROM dual` 返回 `0` | +| 原始发行包 | 用户本机 `Downloads/flydb-cli-0.3.7.zip`;`bin/flydb version` 返回 `0.3.7` | +| 原始 ZIP SHA-256 | `d5b5eed997d7797265769cc9b8dbd84cab38e796e166bebde40275785c94697a` | +| 驱动 SHA-256 | `327bade775f4f15b97618bc9736a4625def52701cf53f5ef5ad72580797e6e45` | +| 本地修复包 | `flydb-cli/target/flydb-cli-0.3.7-local.dm8.20261021.zip`,不包含 JDBC 驱动 | +| 本地修复 ZIP SHA-256 | `cbd18c1c49884123f3bf927aa28b4e27dd9cfd9b9222e69a9675a778d3885ff9` | +| CLI 运行环境 | Java 8;源码构建与只读契约使用 JDK 17 | + +原 CLI 位于仓库下 `target/dm8-validation-20261021/flydb-cli-0.3.7/bin/flydb`,修复 CLI 位于 `target/dm8-validation-20261021/patched/flydb-cli-0.3.7-local.dm8.20261021/bin/flydb`。两者共用测试根目录下的 `flydb.conf` 与 `migration/`,驱动仅复制到本地解压目录。 + +## 写入范围与脚本 + +用户明确授权:创建测试表、视图及专用历史表/锁表,写入两行虚构数据,检查重复迁移;保留测试对象,不执行删除。写入前查询 `USER_OBJECTS` 确认以下四个名称均不存在: + +- `FLYDB_DM8_E2E_20261021`:测试表。 +- `FLYDB_DM8_V_20261021`:测试视图。 +- `flydb_dm8_hist_20261021`:专用历史表。 +- `flydb_dm8_hist_20261021_lock`:专用锁表。 + +配置显式指定 `flydb.database-type=dm`、`flydb.table=flydb_dm8_hist_20261021`、`flydb.driver-download=never`、`flydb.offline=true` 和 `flydb.clean-disabled=true`。`flydb.locations` 为测试目录的绝对路径,不使用项目业务迁移目录。 + +`V1__create_probe.sql`: + +```sql +CREATE TABLE FLYDB_DM8_E2E_20261021 ( + ID INT PRIMARY KEY, + LABEL VARCHAR(100) +); +INSERT INTO FLYDB_DM8_E2E_20261021 (ID, LABEL) VALUES (1, '达梦连接验证'); +``` + +`V2__plsql_probe.sql`: + +```sql +BEGIN + INSERT INTO FLYDB_DM8_E2E_20261021 (ID, LABEL) VALUES (2, 'PLSQL;分号切分验证'); + UPDATE FLYDB_DM8_E2E_20261021 SET LABEL = '达梦迁移验证' WHERE ID = 1; +END; +/ +``` + +`R__probe_view.sql`: + +```sql +CREATE OR REPLACE VIEW FLYDB_DM8_V_20261021 AS +SELECT ID, LABEL FROM FLYDB_DM8_E2E_20261021; +``` + +## 命令与结果 + +以下命令均从测试根目录执行,`CLI` 指向对应解压包的 `bin/flydb`;密码已由 `FLYDB_PASSWORD` 环境注入。每项除注明的缺陷复现外,进程退出码和 JSON `exitCode` 均为 `0`,`protocolVersion=1`、`status=success`。 + +| CLI / 命令 | 结果 | +|---|---| +| 原包:`$CLI --json version` | `0.3.7` | +| 原包:`$CLI -c flydb.conf --json --color=never info` | 识别为“达梦 DM8”,3 个脚本均为 `PENDING` | +| 原包:`$CLI -c flydb.conf --json --color=never validate` | 通过 | +| 原包:`$CLI -c flydb.conf --json --color=never --dry-run migrate` | 3 个脚本、4 条语句;PL/SQL 块为完整 1 条语句 | +| 原包:`$CLI -c flydb.conf --json --color=never migrate` | 依次成功执行 V1、V2、R,版本到 `2` | +| 原包:再次 `info` / `validate` | 3 条 `SUCCESS`,校验通过 | +| 原包:第二次 `migrate` | **退出码 1**,`FLYDB-2010`,历史表已存在;未进入业务脚本执行 | +| 修复包:`$CLI --json version` | `0.3.7-local.dm8.20261021` | +| 修复包:`info` / `validate` / `--dry-run migrate`(同一配置) | 3 条成功历史,预演 0 个脚本、0 条语句 | +| 修复包:连续两次 `migrate`(同一配置) | 均 `executed:[]`、`targetVersionReached:"2"` | +| 修复包:最终 `info` / `validate`(同一配置) | 仍为 3 条 `SUCCESS`,无失败记录,校验通过 | + +首次预演的计划摘要为 `a92549b7cdb12b5de5c881119879f1d413f3f89ebb2e579c5c5e01bb817f3f41`,执行前再次预演得到相同摘要,并逐项核对脚本顺序与语句数。迁移后的零变更摘要为 `e24115cb86ab9e785ca5eab4fa4f36e645805cc5afc9adb6bf3a65d69cfd2a63`。 + +通过独立 JDBC 查询核验:测试表和视图均返回以下两行,历史表仍只有 V1、V2、R 三行,`success=1`。 + +| ID | LABEL | +|---|---| +| 1 | 达梦迁移验证 | +| 2 | PLSQL;分号切分验证 | + +## 缺陷与修复 + +最小复现为对已存在的专用历史表调用 `SchemaHistory.ensureExists()`。原 JDBC 异常为 `vendorCode=-2124`、`SQLState=22000`,消息为“对象[flydb_dm8_hist_20261021]已存在”;原实现只忽略 Oracle `955`,因此重复运行在准备历史表阶段失败。 + +修复新增 `SchemaHistoryDdl.isTableAlreadyExists(SQLException)` 默认方法,保留原有 `955` 行为;`DmDatabase` 额外识别 `-2124`。仓储仅调用方言判定,其他建表错误继续上报,不按 `22000` 整体忽略。默认方法保持现有 SPI 实现的兼容性,未新增 core 运行时依赖。设计文档同步说明实际使用的“尝试建表 + 方言异常判定”流程。 + +回归先于实现添加:原实现 14 项相关测试中 2 项达梦幂等测试报错;修复后通过,并覆盖大小写敏感/不敏感的离线分支、其他同 SQLState 错误不被忽略,以及 Oracle `955` 保持原有行为。大小写敏感分支只有单元测试证据。 + +## 本地验证与原始证据 + +```bash +JAVA_HOME=/path/to/jdk17 ./mvnw -o -B -pl flydb-core -am verify +JAVA_HOME=/path/to/jdk17 ./mvnw -o -B -Drevision=0.3.7-local.dm8.20261021 verify +JAVA_HOME=/path/to/jdk8 ./mvnw -o -B -pl flydb-core -am verify +./scripts/check-bytecode.sh 52 \ + flydb-core/target/classes flydb-cli/target/classes \ + flydb-runtime/target/classes flydb-web/target/classes \ + flydb-spring-boot-2-starter/target/classes examples/boot2-demo/target/classes +./scripts/check-bytecode.sh 61 \ + flydb-spring-boot-3-starter/target/classes examples/boot3-demo/target/classes +``` + +core 在 JDK 17 和 JDK 8 下均为 363 项测试通过,行覆盖率 80% 门禁通过;JDK 17 完整 reactor 验证及 Java 8/17 字节码边界检查通过。完整 reactor 中未配置实例的厂商契约按条件跳过;另行启用的达梦真库只读契约实际执行 1 项,失败 0、跳过 0,具体命令见 [达梦指南](../getting-started/dm8.md)。 + +原始 JSON、stderr、退出码及构建日志保留在本机 `target/dm8-validation-20261021/logs/`,其中包括: + +- `licensed-contract.log`:真库只读契约。 +- `dry-run-before.json`、`migrate-first.stdout`、`jdbc-after-first.stdout`:原包预演、首次写入及核验。 +- `migrate-second.stdout`、`history-init-repro.stdout`:原包重复迁移失败及原始 JDBC 错误码。 +- `patched-*.stdout`、`patched-*.exit-code`:修复包的预演、两次零变更迁移及最终 JDBC 核验。 +- `regression-before-fix.log`、`core-verify.log`、`reactor-verify.log`:失败回归与本地构建验证。 +- `java8-core-verify.log`:JDK 8 下的 core 测试与覆盖率验证。 + +`target/` 为忽略目录,原始日志不随仓库提交;本记录保留可审查的输入、产物身份与关键结果。测试对象仍留在本地 `SYSDBA` schema,清理需另行确认明确范围。 + +## 0.3.8 发布候选复验 + +发布准备基于最新 main `06baf0c7faefb8a6060050c292898bedd39b7b9b`,包含其上已合并的依赖更新,再加入本次达梦修复及版本同步。JDK 17 完整 `./mvnw -B verify`、CI 对应的 JDK 8 模块矩阵 verify 均通过;MCP Adapter 类型检查、58 项单元测试及构建通过,真实 0.3.8 CLI 的 MCP 端到端测试为 7 项通过、5 项写入 profile 按条件跳过。 + +从新构建的 `flydb-cli-0.3.8.zip` 解压候选包后,`bin/flydb --json version` 返回 `0.3.8`。在同一授权配置与测试对象上再次执行 info、validate、dry-run、连续两次 migrate、info、validate:所有进程及 JSON 退出码均为 0,两次 migrate 均为 `executed:[]`、当前版本 `2`。独立 JDBC 查询确认测试表和视图仍为上述两行,历史记录仍为三条成功,无新增失败记录。 + +候选包位于本机 `target/release-0.3.8/candidate/flydb-cli-0.3.8/`;日志保留在 `target/release-0.3.8/logs/`(`reactor-verify.log`、`java8-verify.log`、`mcp-verify.log`、`mcp-e2e.log`、`dm8-candidate.log` 及 `release-*.stdout`)。ZIP 核对包含当前源码构建的 MCP bundle,不含厂商 JDBC 驱动;驱动仅在本地解压后加入测试目录。 diff --git a/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java b/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java index af82927..b2ca8ec 100644 --- a/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java +++ b/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java @@ -750,7 +750,7 @@ private static void put(Map 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