已合并
【实习#28】新增 SpringDataJPA 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #111
【实习#28】新增 SpringDataJPA 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #111
已合并
wsrym创建于 29 天前
共 22 个文件变更+1828-0
@@ -0,0 +1,94 @@
1+# Spring Data JPA 连接 openGauss B 兼容库
2+ 
3+对应任务:Issue #28 — openGauss B 库 B 协议适配 MySQL Java 框架 SpringDataJPA。
4+ 
5+使用 Spring Boot 3.x + Spring Data JPA(Hibernate 6)配合 MySQL JDBC 驱动连接 openGauss B 兼容模式数据库的示例工程,包含 19 个测试用例、7 档驱动版本矩阵,以及测试思维导图与自测报告。
6+ 
7+## 目录结构
8+ 
9+```
10+springdatajpa-connect-opengauss-b
11+├── pom.xml # 含 7 档驱动 profile(版本矩阵用)
12+├── README.md
13+├── docs
14+│ ├── 测试思维导图-SpringDataJPA连接openGaussB库.{mm,md,html}
15+│ └── 基于SpringDataJPA框架的openGauss数据库测试.md # 自测报告
16+└── src
17+ ├── main/java/org/opengauss/smoke # 实体/仓储/方言/示例应用
18+ └── test/java/org/opengauss/smoke/jpacases # 测试套件(19 用例)
19+```
20+ 
21+## 前置条件
22+ 
23+与 `springboot-connect-opengauss-b`(Issue #27 工程)相同:JDK 17+、openGauss MySQL 协议已开(3308)、B 库/schema/`` `user` `` 表/用户已就绪。环境搭建步骤详见 docs 仓《[SpringDataJPA 连接示例](https://docs.opengauss.org/zh/docs/latest/mysql_compatibility_developer_guide/springdatajpa_development.html)》。
24+ 
25+## 配置
26+ 
27+| 变量 | 默认值 | 说明 |
28+|---|---|---|
29+| DB_HOST | 127.0.0.1 | openGauss 所在机器的 IP |
30+| DB_PORT | 3308 | dolphin 协议端口 |
31+| DB_NAME | mysql_test_db | B 库下 schema(等价 MySQL database) |
32+| DB_USER / DB_PASSWORD | mysql_test_db / ChangeMe_123 | MySQL 认证用户 |
33+ 
34+## 运行
35+ 
36+```bash
37+# 运行全部测试(19 用例)
38+mvn test
39+ 
40+# 运行示例主程序
41+mvn spring-boot:run
42+```
43+ 
44+## 测试内容(19 用例)
45+ 
46+| 类别 | 用例 | 覆盖点 |
47+|---|---|---|
48+| 已知差异回归 | K1 / K2 | K1 复现转义语义差异(派生 LIKE escape 报错,回归断言,与 Plugin 仓 issue #504 同源);K2 验证 workaround(JPQL 显式 `escape '!'`) |
49+| ddl-auto 专项 | D1 / D2 | D1 update 模式(FINDING E 静默失效);D2 validate 模式 |
50+| 类型映射 | M1 | BigDecimal/LocalDate/LocalDateTime/Boolean/String 往返 + IDENTITY 主键回填 |
51+| 分页与批量 | P1 / P2 | PageRequest 多页遍历 / saveAll(50) 批量回填 |
52+| 冒烟 | 3 | saveAndFind / pagination / transactionRollback |
53+| 协议命令 | C1-C9 | COM_QUERY/INIT_DB/PING/STMT_PREPARE/STMT_EXECUTE/STMT_CLOSE/STMT_SEND_LONG_DATA/STMT_RESET/STMT_FETCH/SET_OPTION/QUIT(评审意见落地,与 #27 报告 7.1-7.9 同设计) |
54+ 
55+## 驱动版本矩阵(7 档 × 19 用例,三档行为)
56+ 
57+| 驱动版本 | 结果 |
58+|---|---|
59+| 8.0.28(文档基准) | 协议类 1 失败 + 6 错误(FINDING I 崩溃 + FINDING H 丢 8KB),其余 12 通过(含 K1/D1 复现) |
60+| 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0 | 18/19(仅 FINDING H;RESET 正常、服务端不崩) |
61+| 9.7.0(Boot 默认)/ 26.7.0(最新) | 19/19 全部通过 |
62+ 
63+切换档位重跑:
64+ 
65+```bash
66+mvn test -Pdriver-8.0.28
67+mvn test -Pdriver-8.0.33
68+mvn test -Pdriver-8.1.0
69+mvn test -Pdriver-8.3.0
70+mvn test -Pdriver-8.4.0
71+mvn test -Pdriver-9.7.0
72+mvn test -Pdriver-26.7.0
73+```
74+ 
75+> 三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常。核心链路与 K1/D1/FINDING E/F/G 均与驱动版本无关;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本。
76+>
77+> **注意:8.0.28 档跑协议命令测试会打崩 openGauss 服务端进程(FINDING I),需重启数据库后才可继续。**
78+ 
79+## 已知要点与限制
80+ 
81+## 已知要点与限制
82+ 
83+| 项 | 说明 |
84+|---|---|
85+| 转义语义差异(issue #504) | Hibernate 6.6 默认给 LIKE 追加 `escape '\\'`(两个反斜杠),B 模式视 `'\\'` 为 2 字符 → `invalid escape string`。根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。规避:JPQL 显式 `escape '!'`(见 UserRepository.searchByName)。与驱动版本无关 |
86+| FINDING E | ddl-auto=update 静默失效(HHH000339:information_schema.KEYWORDS 不存在(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感:大写报错、小写可查)→ 不建表也不报错)。**设置 `dolphin.lower_case_table_names=1` 并持久化后实测 Hibernate 元数据探测恢复**;未配置时建议手工建表 + `ddl-auto: none` |
87+| FINDING F | COM_STMT_SEND_LONG_DATA(服务端预编译)写入含 0x00 的二进制被按 C 字符串截断为前缀(无零字节 1MB 完整往返 MD5 一致;客户端预编译路径无此问题) |
88+| FINDING G | allowMultiQueries 多语句后续 SELECT 结果集经 getMoreResults() 不可得(仅首语句结果 + 末 DML update count;DML 均落库) |
89+| FINDING H | COM_STMT_SEND_LONG_DATA 无零字节 1MB:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整 |
90+| FINDING I | COM_STMT_RESET(clearParameters 后复用):仅 8.0.28 档,第二次 executeUpdate 直接崩溃 openGauss 服务端进程(EOFException 断连、3308 不可达、可复现 3 次);8.0.33 及以上档正常 |
91+| 方言 | `database-platform: org.opengauss.smoke.OpenGaussBMySQLDialect`(继承 MySQLDialect,声明无反斜杠转义语义,作为适配点) |
92+| jakarta | Spring Boot 3.x 实体类 import 用 `jakarta.persistence.*`(不是 javax) |
93+ 
94+完整分析见 `docs/基于SpringDataJPA框架的openGauss数据库测试.md`。
@@ -0,0 +1,300 @@
1+# 基于SpringDataJPA框架的openGauss数据库测试
2+ 
3+版权所有 © 2026 openGauss社区
4+ 您对“本文档”的复制、使用、修改及分发受知识共享(Creative Commons)署名—相同方式共享4.0国际公共许可协议(以下简称“CC BY-SA 4.0”)的约束。为了方便用户理解,您可以通过访问[*https://creativecommons.org/licenses/by-sa/4.0/*](https://creativecommons.org/licenses/by-sa/4.0/) 了解CC BY-SA 4.0的概要 (但不是替代)。CC BY-SA 4.0的完整协议内容您可以访问如下网址获取:[*https://creativecommons.org/licenses/by-sa/4.0/legalcode*](https://creativecommons.org/licenses/by-sa/4.0/legalcode)。
5+ 
6+修订记录
7+ 
8+| 日期 | 修订版本 | 修改描述 | 作者 |
9+| --------- | -------- | ------------------------------------------------- | ---- |
10+| 2026-08-28 | 1.0 | openGauss 对 Spring Data JPA + MySQL JDBC 的兼容性测试 | lele-2026 |
11+| 2026-09-04 | 1.1 | 协议命令专项 9 用例实测结果回填(新增 FINDING F/G);LIKE escape 转义语义差异表述与 docs 仓指导文档对齐(escape '\\' 两个反斜杠、与 Plugin 仓 issue #504 同源);FINDING E 定位修正(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感,=1 持久化可恢复) | lele-2026 |
12+| 2026-09-04 | 1.2 | 驱动矩阵全量补测:协议命令行为按驱动版本分三档(新增 FINDING H/I,与 #27 报告同源)——SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档)、COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档,可复现);9.7.0/26.7.0 档 19/19 全绿;矩阵结论由"行为逐项一致"改写为三档行为 | lele-2026 |
13+| 2026-09-04 | 1.3 | 转义字符问题不再以 Bug 描述:全文统一为"转义语义差异(已知差异,与 Plugin 仓 issue #504 同源)",与 docs 仓指导文档口径一致(涉及摘要、3.1/3.3/4.1.1/4.2/5.1/5.2/6.1/6.2/7.1/7.2/7.3、思维导图、README 与测试代码注释) | lele-2026 |
14+| 2026-09-04 | 1.4 | 删除 7.1 遗留问题列表(内部跟踪事项不再在公开报告列出,各差异/FINDING 的现状与规避已完整记录于 3.3/5.2;与 #27 报告同步处理);附件 7.2/7.3 重编号为 7.1/7.2;环境表移除内网 IP;附件表移除"原始日志备存"行 | lele-2026 |
15+ 
16+[TOC]
17+ 
18+**关键词**:
19+ 
20+Spring Data JPA,Hibernate,MySQL JDBC,openGauss 数据库,B 兼容模式,dolphin 插件,兼容性测试
21+ 
22+**Abstract 摘要**:
23+ 
24+本测试验证使用 MySQL JDBC 驱动(mysql-connector-j)连接 openGauss B 兼容模式数据库,在 Spring Data JPA(Hibernate 6.6)框架下的兼容性。测试覆盖已知差异回归、ddl-auto 行为、类型映射、分页与批量、冒烟、协议命令六个方面共 19 个用例,并扫描 7 个驱动版本验证版本边界。结果显示:Spring Data JPA 在 openGauss B 模式下类型映射、主键回填、分页、批量、事务等核心链路均可正常工作;发现并定位 6 个问题——LIKE 模糊查询 escape 转义语义差异(已有 workaround,与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)、FINDING E(ddl-auto=update 静默失效,根因为默认配置 `dolphin.lower_case_table_names=0` 下 information_schema 表名大小写敏感(Hibernate 发大写 KEYWORDS 报不存在,小写 keywords 实测可查),设置 =1 并持久化后实测恢复)、FINDING F(COM_STMT_SEND_LONG_DATA 二进制参数遇 0x00 截断)、FINDING G(多语句结果返回结构不完整)、FINDING H(SEND_LONG_DATA 1MB 丢末块 8KB,8.0.28~8.4.0 档)与 FINDING I(COM_STMT_RESET 崩溃服务端进程,仅 8.0.28 档)。该转义差异与 FINDING E/F/G 均与驱动版本无关,根源在服务端;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本。
25+ 
26+**缩略语清单:**
27+ 
28+| 缩略语 | 英文全名 | 中文解释 |
29+| ------ | --------------------------- | -------------- |
30+| JPA | Jakarta Persistence API | Java 持久化 API |
31+| ORM | Object Relational Mapping | 对象关系映射 |
32+| JPQL | Jakarta Persistence Query Language | JPA 查询语言 |
33+| ddl-auto | Hibernate DDL 自动管理配置 | 建表策略配置 |
34+ 
35+***
36+ 
37+# 1 概述
38+ 
39+本报告针对 Spring Data JPA 框架(Hibernate 6 实现)配合 MySQL JDBC 驱动与 openGauss B 兼容模式数据库的兼容性测试活动进行总结。Spring Data JPA 是 Spring Boot 生态最常用的 ORM 方案,其兼容性风险集中在方言适配、元数据探测、主键生成与查询渲染四个环节,本测试针对性地覆盖了这些风险点。
40+ 
41+# 2 测试版本说明
42+ 
43+## 2.1 测试版本信息
44+ 
45+### 2.1.1 被测版本
46+ 
47+| 版本名称 | 软件包名称 | 测试起始时间 | 测试结束时间 | 测试人员 |
48+| ---------- | ----------------------------- | ------------ | ------------ | -------- |
49+| Spring Boot | spring-boot-starter-data-jpa 3.5.16 | 2026-08-28 | 2026-08-28 | lele-2026 |
50+| Hibernate | hibernate-core 6.6.53(Boot BOM 管理) | 2026-08-28 | 2026-08-28 | lele-2026 |
51+| mysql-connector-j | 9.7.0(Boot BOM 默认) | 2026-08-28 | 2026-08-28 | lele-2026 |
52+| openGauss | openGauss 7.0.0-RC3(源码编译,commit fe6f6e4b) | 2026-08-28 | 2026-08-28 | lele-2026 |
53+ 
54+> 说明:任务要求基于 openGauss 7.0.0-LTS,实测环境为 7.0.0-RC3(与 issue 开发指导文档同版本),正式版发布后建议回归。
55+>
56+> 说明:被测二进制基于 7.0.0-RC3(commit fe6f6e4b)的本地源码工作副本编译:dolphin 插件为本地 contrib/dolphin 副本,包含此前 round() 支持 blob 任务的本地改动(已核验二进制中存在 round(binary/blob) 系列函数签名),contrib/timescaledb 未参与编译。上述差异均不在本测试覆盖的 MySQL 协议 / JDBC / 类型映射 / 事务路径上,不影响测试结论。
57+ 
58+### 2.1.2 配套测试的版本
59+ 
60+| 版本名称 | 配套版本 | 版本说明 |
61+| -------- | --------------- | ----------------------------------------------- |
62+| JDK | 17.0.15(Liberica) | Spring Boot 3.x 要求 JDK 17+ |
63+| Maven | 3.9.14 | 构建工具 |
64+| 驱动矩阵 | 7 个版本 | 8.0.28(旧坐标)/ 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0 / 9.7.0 / 26.7.0 |
65+ 
66+## 2.2 测试环境描述
67+ 
68+### 2.2.1 环境硬件信息
69+ 
70+| 环境信息 | 硬件配置信息 | 备注 |
71+| -------- | ------------------------------------------------------------ | ----------------- |
72+| 开发机 | OS:Windows 11<br />JDK 17.0.15<br />Maven 3.9.14 | 测试代码执行环境 |
73+| 虚拟机 | OS:openEuler 20.03 LTS(x86_64)<br />openGauss 源码编译安装(debug) | 数据库环境 |
74+ 
75+### 2.2.2 虚拟化平台
76+ 
77+| 虚拟化平台 | 版本说明 |
78+| ------------------ | -------- |
79+| VMware Workstation Pro | NAT 网络模式,Windows 开发机可直连虚拟机 |
80+ 
81+### 2.2.3 OS版本
82+ 
83+| 操作系统 | OS版本 | 版本说明 |
84+| --------- | ----------- | ------------------------ |
85+| openEuler | 20.03 (LTS) | x86_64 版本 |
86+ 
87+### 2.2.4 数据库环境配置
88+ 
89+| 配置项 | 值 |
90+| ----------------------------- | ----------------------------------------- |
91+| 协议开关 | `enable_dolphin_proto = on` |
92+| MySQL 协议端口 | `dolphin_server_port = 3308` |
93+| 默认数据库 | `dolphin.default_database_name = 'proto_test_db'` |
94+| 测试数据库 | `proto_test_db`(DBCOMPATIBILITY 'B') |
95+| 测试用户 | `mysql_test_db`(已执行 `set_native_password` 原生密码认证) |
96+| 客户端接入认证 | pg_hba.conf:`host all mysql_test_db 0.0.0.0/0 sha256` |
97+| 测试表 | `` `user` ``(预置 3 行)、t_jpa_map(类型映射)、t_jpa_ddl(ddl-auto 专项)、jpa_cmd(协议命令专项,含 LONGBLOB 列,预置 500 行) |
98+ 
99+# 3 版本概要测试结论、关键风险和规避措施
100+ 
101+## 3.1 测试结论总结
102+ 
103+本测试共设计 19 个测试用例,覆盖六个方面:已知差异回归(2)、ddl-auto 专项(2)、类型映射(1)、分页与批量(2)、冒烟(3)、协议命令专项(9)。**19 个用例在 BOM 默认档(9.7.0)全部通过**(已知差异用例的"通过"= 复现了预期中的当前差异行为)。
104+ 
105+核心结论:
106+ 
107+1. Spring Data JPA 在 openGauss B 模式下的核心链路——类型映射、IDENTITY 主键回填、分页、批量保存、事务回滚——全部可用;
108+2. **LIKE 模糊查询 escape 转义语义差异**:Hibernate 6.6 默认在所有 LIKE 谓词后追加 `escape '\\'`(两个反斜杠),MySQL 中 `'\\'` 是 1 个字符,而 B 模式按标准 SQL 把 `'\\'` 视为 2 个字符,导致 `invalid escape string`。规避:JPQL 显式 `escape '!'`。该差异的根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504),与驱动版本无关;
109+3. **FINDING E(ddl-auto 静默失效)**:Hibernate 启动时查询 `information_schema.KEYWORDS` 失败(仅警告)→ ddl-auto=update 不做任何建表动作且不报错,运行时才暴露 `relation does not exist`。根因为默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感(视图存在、小写可查)。**设置 `dolphin.lower_case_table_names=1` 并持久化后实测大写 KEYWORDS 可查、Hibernate 元数据探测恢复**;未配置时建议 B 模式手工建表 + `ddl-auto=none`;
110+4. **FINDING F / G(协议命令专项)**:COM_STMT_SEND_LONG_DATA 写入含 0x00 的二进制被按 C 字符串截断为前缀;allowMultiQueries 多语句后续 SELECT 结果集经 getMoreResults() 不可得(均详见 5.2.2,回归断言);
111+5. **协议命令行为按驱动版本分三档**:8.0.28 档 COM_STMT_RESET 可复现崩溃服务端进程(FINDING I)、8.0.28~8.4.0 档 SEND_LONG_DATA 1MB 丢末块 8KB(FINDING H)、9.7.0/26.7.0 档 19/19 全部通过;该转义差异与 FINDING E/F/G 在 7 档下行为一致、与驱动版本无关(详见 4.2)。
112+ 
113+## 3.2 约束说明
114+ 
115+1. ddl-auto=create 模式未测(会 drop+create 全部映射表,破坏现有数据);其行为预计与 update 一致(同样依赖元数据探测);
116+2. Hibernate 批量参数(hibernate.jdbc.batch_size / order_inserts)未专项计时,saveAll 仅验证功能正确性;
117+3. 测试基于 openGauss 7.0.0-RC3,正式 7.0.0-LTS 发布后建议用同一套用例回归。
118+ 
119+## 3.3 关键风险和规避措施
120+ 
121+| 风险 | 说明 | 规避措施 |
122+| ---- | ---- | -------- |
123+| 转义语义差异:LIKE 模糊查询报错 | 派生查询 / 隐式 escape 渲染 `escape '\\'`(两个反斜杠)→ `invalid escape string` | JPQL 显式 `escape '!'`(本工程 searchByName 为参考实现);根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504);若服务端调整反斜杠转义语义(B 模式字符串字面量支持反斜杠转义)该差异可消除 |
124+| FINDING E:ddl-auto=update 静默失效 | 容器启动成功但不建表,运行时才报错,排查困难 | **设置 `dolphin.lower_case_table_names=1` 并持久化后实测 Hibernate 元数据探测恢复**;未配置时 B 模式手工建表 + `ddl-auto=none`(或 validate) |
125+| FINDING A 联动:元数据探测受限 | getTables / prepareCall 依赖 information_schema.TABLES/PARAMETERS | 同 #27 报告 FINDING A:默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感,设置 =1 并持久化后实测 `DatabaseMetaData.getTables` 恢复正常;ORM 场景尽量避免依赖元数据探测的功能 |
126+| FINDING F:SEND_LONG_DATA 二进制参数遇 0x00 截断 | 服务端预编译 + setBlob 流式写入 LONGBLOB 时,二进制遇第一个 0x00 字节被按 C 字符串截断为前缀(无零字节 1MB 完整往返 MD5 一致;客户端预编译文本协议路径无此问题) | 服务端预编译路径避免写入含 0x00 的二进制(用客户端预编译文本协议路径);服务端修复后回归 |
127+| FINDING G:多语句结果返回结构不完整 | allowMultiQueries=true 多语句执行时 getMoreResults() 取不到后续 SELECT 结果集(仅首语句结果 + 末 DML update count;纯 SELECT 序列逐个正常;DML 均落库) | 逐条执行或仅依赖末条 update count;服务端修复后回归 |
128+| FINDING H:SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档,与 #27 报告同源) | 服务端预编译 + setBlob 写入 1MB 全 0x41(无零字节)数据时,8.0.28/8.0.33/8.1.0/8.3.0/8.4.0 档读回仅 1040384 字节 = 127×8192(恰少末块 8192),MD5 校验失败;9.7.0/26.7.0 档完整往返 | 8.0.28~8.4.0 档服务端预编译路径避免写入 8192 倍数大小的大二进制(改用 9.7.0+ 驱动或客户端预编译文本协议路径);服务端修复后回归 |
129+| FINDING I:COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档,与 #27 报告同源) | 8.0.28 档 clearParameters 后二次 executeUpdate(驱动先发 COM_STMT_RESET)直接崩溃 openGauss 服务端进程:客户端报 `EOFException: read 0 bytes before connection was unexpectedly lost`,3308 端口不可达需人工重启;单测可复现(两个工程同构);8.0.33 及以上档同一序列正常 | 8.0.28 档避免服务端预编译语句 clearParameters 后复用(新建语句或换 8.0.33+ 驱动);服务端修复后回归 |
130+ 
131+# 4 版本详细测试结论
S
Ssuperlchf26 天前

【检视意见】增加点基础功能测试,比如COM_STMT_PREPARE、COM_STMT_SEND_LONG_DATA、COM_STMT_EXECUTE 最好覆盖到每个command

likedislike
132+ 
133+## 4.1 特性测试结论
134+ 
135+### 4.1.1 已知差异回归(2 用例,符合预期)
136+ 
137+| 用例 | 结果 |
138+| ---- | ---- |
139+| K1:派生 LIKE(findByNameLike) | ✓ 复现转义语义差异:`invalid escape string`(Hibernate 6.6 默认追加 `escape '\\'`(两个反斜杠),MySQL 中 `'\\'`=1 字符,B 模式按标准 SQL 视 `'\\'` 为 2 字符;根源是 B 模式不支持反斜杠作为字符串转义字符,与 Plugin 仓 issue #504 同源)。**本用例为回归断言**:服务端行为调整后测试将自动失败,提醒移除 workaround 并更新文档 |
140+| K2:JPQL 显式 `escape '!'`(searchByName) | ✓ workaround 稳定可用(分页版模糊查询) |
141+ 
142+### 4.1.2 ddl-auto 专项(2 用例)
143+ 
144+| 用例 | 结果 |
145+| ---- | ---- |
146+| D1:ddl-auto=update | **FINDING E**:容器启动成功,但缺失表 t_jpa_ddl 未被创建,不报错不警告(静默失效)。日志证据:`HHH000339: Could not obtain connection metadata ... relation "information_schema.KEYWORDS" does not exist on primary`(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感:视图存在、小写 keywords 实测可查)→ 元数据获取失败 → 无法做"现有 schema vs 实体"对比 → update 什么都不做。设置 `dolphin.lower_case_table_names=1` 并持久化后实测大写 KEYWORDS 可查、元数据探测恢复 |
147+| D2:ddl-auto=validate | 启动校验未报错(但元数据同样缺失,校验深度存疑,如实表述"未报错") |
148+ 
149+### 4.1.3 类型映射(1 用例,通过)
150+ 
151+| 用例 | 结果 |
152+| ---- | ---- |
153+| M1:五类型往返 + 主键回填 | ✓ BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 写入读出一致;save 后 IDENTITY 主键回填正常(getGeneratedKeys 链路可用) |
154+ 
155+### 4.1.4 分页与批量(2 用例,通过)
156+ 
157+| 用例 | 结果 |
158+| ---- | ---- |
159+| P1:PageRequest 多页遍历 | ✓ LIMIT/OFFSET 与 count 查询正常 |
160+| P2:saveAll(50) | ✓ 批量保存 50 条主键逐个回填 |
161+ 
162+### 4.1.5 冒烟(3 用例,通过)
163+ 
164+| 用例 | 结果 |
165+| ---- | ---- |
166+| saveAndFind / pagination / transactionRollback | ✓ 全链路正常;@Transactional 异常回滚正常 |
167+ 
168+### 4.1.6 协议命令专项(9 用例)
169+ 
170+> 背景:评审意见要求增加协议命令基础功能测试(COM_STMT_PREPARE / COM_STMT_SEND_LONG_DATA / COM_STMT_EXECUTE,最好覆盖到每个 command)。MySQL 协议命令按"JDBC 是否有触发路径"处理:可触发的逐条实测(用例 C1-C9),无触发路径的在下方说明。与 #27 报告(springboot)7.1-7.9 同一套设计。
171+ 
172+| 协议命令 | JDBC 触发方式 | 用例 | 结果 |
173+| -------- | ------------- | ---- | ---- |
174+| COM_QUERY | 普通 Statement 执行 SQL(文本协议,建表/插入/查询均走此命令) | C1 | ✓ 插入 2 行后计数 +2(文本协议插入/查询/计数正常) |
175+| COM_INIT_DB | url 指定 database 建立连接(握手携带);setCatalog + useLocalSessionState=false | C2 | ✓ 连接 catalog=测试库;setCatalog 切换后查询正常 |
176+| COM_PING | Connection.isValid(2) | C3 | ✓ isValid=true |
177+| COM_STMT_PREPARE | useServerPrepStmts=true 下 prepareStatement | C4 | ✓ 服务端预编译插入+查询往返正常(cachePrepStmts=false 确保真实发出) |
178+| COM_STMT_EXECUTE | 服务端预编译语句 executeQuery/executeUpdate | C4 | ✓ 同 C4 |
179+| COM_STMT_CLOSE | 服务端预编译语句 close | C4 | ✓ 同 C4(语句关闭无异常) |
180+| COM_STMT_SEND_LONG_DATA | 服务端预编译 + setBlob 流式写入 1MB LONGBLOB(MD5 往返校验) | C5 | ✓ 链路可用(9.7.0 基线),但发现 FINDING F:无零字节 1MB(全 0x41)完整往返 MD5 一致;含 0x00 的 200 字节读回仅 150 字节(= 第一个 0x00 前的前缀)——服务端对二进制参数按 C 字符串截断(与 #27 报告 FINDING F 同源,详见 5.2.2,回归断言)。驱动矩阵补测发现 FINDING H:8.0.28~8.4.0 档无零字节 1MB 读回仅 1040384 字节(丢末块 8KB,详见 4.2/5.2.2) |
181+| COM_STMT_RESET | clearParameters 后复用同一预编译语句 | C6 | ✓(8.0.33 及以上档)clearParameters 后复用同一语句插入 2 行均生效;但 8.0.28 档此序列(COM_STMT_RESET)可复现崩溃服务端进程(FINDING I,详见 5.2.2) |
182+| COM_STMT_FETCH | useCursorFetch=true + setFetchSize(100) | C7 | ✓ 驱动层无报错、完整读回 500 行;协议层是否真逐批返回需服务端日志佐证(口径同 #27 报告 6.1,按"驱动层可用、协议层待确认"记录) |
183+| COM_SET_OPTION | allowMultiQueries=true 多语句执行 | C8 | ✓ 参数生效、多语句 DML 全部落库,但发现 FINDING G:execute() 仅返回首语句 update count,getMoreResults() 取不到后续 SELECT 结果集(后续 SELECT 被丢弃;纯 SELECT 序列逐个正常)(与 #27 报告 FINDING G 同源,详见 5.2.2,回归断言) |
184+| COM_QUIT | Connection.close | C9 | ✓ close 后 isClosed=true |
185+ 
186+JDBC 无触发路径的命令:COM_FIELD_LIST(已被查询结果集元数据取代)、COM_PROCESS_INFO / COM_STATISTICS / COM_PROCESS_KILL(管理类命令)、COM_CHANGE_USER、COM_REFRESH 等,MySQL JDBC 驱动不提供对应 API,无法从应用层触发,不纳入用例;本专项已覆盖**驱动可触发的全部协议命令**。
187+ 
188+## 4.2 兼容性测试结论(驱动版本矩阵)
189+ 
190+同一套 19 用例扫描 7 档驱动(Maven profile 切换,同一服务端环境):
191+ 
192+| 驱动版本 | 坐标 | 结果 |
193+| -------- | ---- | ---- |
194+| 8.0.28(文档基准) | mysql:mysql-connector-java(旧坐标) | 协议命令类 1 失败 + 6 错误:SEND_LONG_DATA 丢末块 8KB(FINDING H)、COM_STMT_RESET 崩溃服务端进程(FINDING I,后续用例全部失联,需人工重启服务端);其余 12 用例通过(含 K1/D1 复现) |
195+| 8.0.33 | com.mysql:mysql-connector-j(新坐标) | 19 用例中 1 失败(FINDING H:SEND_LONG_DATA 丢末块 8KB),其余 18 通过(COM_STMT_RESET 正常、服务端未崩溃) |
196+| 8.1.0 | 同上 | 同上(1 失败 + 18 通过) |
197+| 8.3.0 | 同上 | 同上(1 失败 + 18 通过) |
198+| 8.4.0 | 同上 | 同上(1 失败 + 18 通过) |
199+| 9.7.0(Boot 3.5.16 BOM 默认) | 同上 | 19/19 通过 |
200+| 26.7.0(最新) | 同上 | 19/19 通过 |
201+ 
202+7 档驱动的核心链路(类型映射/主键回填/分页/批量/事务)与 K1 复现转义语义差异、D1 复现 FINDING E、多语句结果结构(FINDING G)**逐项一致**——这些问题均与驱动版本无关,根源在服务端;协议命令专项按驱动版本分**三档**:8.0.28 崩溃 / 8.0.33~8.4.0 丢 8KB / 9.7.0 及以上正常(FINDING H/I,详见 5.2.2)。与 #27 报告 8 档矩阵结论互相印证。
203+ 
204+# 5 测试对象质量评估
205+ 
206+## 5.1 覆盖率分析
207+ 
208+| 覆盖维度 | 覆盖情况 |
209+| -------- | -------- |
210+| 功能覆盖 | 已知差异回归 / ddl-auto / 类型映射+主键回填 / 分页 / 批量 / 事务回滚 / 协议命令,共 19 用例 |
211+| 风险点覆盖 | ORM 四大风险环节全覆盖:方言(MySQLDialect+自定义适配点)、元数据探测(D1 捕获 HHH000339)、主键生成(IDENTITY 回填)、查询渲染(K1 捕获 escape 渲染差异) |
212+| 版本覆盖 | 驱动 7 档(8.0.28 ~ 26.7.0,覆盖新旧两套 Maven 坐标) |
213+| 未覆盖项 | ddl-auto=create(破坏性)、Hibernate 批量参数性能、二级缓存、锁模式、JDBC 无触发路径的协议命令(见 4.1.6) |
214+ 
215+## 5.2 缺陷统计和分析
216+ 
217+### 5.2.1 缺陷统计
218+ 
219+| 类别 | 数量 | 说明 |
220+| ---- | ---- | ---- |
221+| 服务端差异(FINDING/已知差异) | 6 | LIKE escape 转义语义差异(与 issue #504 同源)+ FINDING E(ddl-auto 静默失效)+ FINDING F(SEND_LONG_DATA 0x00 截断)+ FINDING G(多语句结果结构不完整)+ FINDING H(SEND_LONG_DATA 1MB 丢末块 8KB,8.0.28~8.4.0 档)+ FINDING I(COM_STMT_RESET 崩溃,仅 8.0.28 档) |
222+| 崩溃类缺陷 | 1 | FINDING I(8.0.28 档 COM_STMT_RESET 可复现崩溃 openGauss 服务端进程,与 #27 报告同源) |
223+ 
224+### 5.2.2 缺陷列表
225+ 
226+| 编号 | 现象 | 定位 | 严重程度 | 规避措施 |
227+| ---- | ---- | ---- | -------- | -------- |
228+| 转义语义差异(#504) | 派生 LIKE 查询报 `invalid escape string` | Hibernate 6.6 默认渲染 `escape '\\'`(两个反斜杠);MySQL 中 `'\\'`=1 字符,B 模式按标准 SQL 把 `'\\'` 当 2 字符。根源是 B 模式不支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。与驱动版本无关(8.0.28/9.7.0 同错) | 高(模糊查询是高频功能) | JPQL 显式 `escape '!'`;服务端候选调整:B 模式字符串字面量支持反斜杠转义(已 gsql 取证,见 7.2 附件) |
229+| FINDING E | ddl-auto=update 容器启动成功但表未创建,静默失效 | Hibernate 查 information_schema.KEYWORDS 失败(HHH000339 仅警告)→ schema 对比无法进行。与 FINDING A(TABLES/PARAMETERS)同源:默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感(视图本身均存在,小写可查) | 高(对按 MySQL 教程配置 ddl-auto=update 的用户是隐性陷阱) | **`dolphin.lower_case_table_names=1` 并持久化(实测 Hibernate 元数据探测恢复)**;未配置时手工建表 + ddl-auto=none |
230+| FINDING F | COM_STMT_SEND_LONG_DATA 写入含 0x00 的二进制被截断 | 服务端预编译路径对二进制参数按 C 字符串处理:遇第一个 0x00 截断为前缀(200 字节读回 150 = 0x00 位置);无零字节 1MB 完整往返(MD5 一致);客户端预编译(文本协议)路径无此问题 | 中(影响服务端预编译 + 二进制大对象写入的正确性) | 服务端预编译路径避免写入含 0x00 的二进制(用客户端预编译文本协议路径) |
231+| FINDING G | 多语句后续 SELECT 结果集经 getMoreResults() 不可得 | `allowMultiQueries=true` 下结果返回结构不完整:仅回传首语句结果 + 末 DML update count,中间结果全部丢弃(纯 SELECT 序列逐个正常);各语句 DML 均已落库 | 中(影响依赖多结果集解析的应用/框架) | 逐条执行或仅依赖末条 update count |
232+| FINDING H | SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档,与 #27 报告同源) | 服务端预编译 + setBlob 写入 1MB 全 0x41(无零字节)数据时,8.0.28~8.4.0 档读回仅 1040384 字节 = 127×8192(恰少末块 8192),MD5 失败(断言行固定 `expected: 1048576 but was: 1040384`);9.7.0/26.7.0 档完整往返。与驱动分块方式(8192 对齐)相关,缺陷在服务端 | 高(服务端预编译 + 大二进制写入数据静默丢失) | 8.0.28~8.4.0 档避免服务端预编译写入 8192 倍数大小的大二进制(改用 9.7.0+ 驱动或客户端预编译文本协议路径) |
233+| FINDING I | COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档,与 #27 报告同源) | 8.0.28 档 clearParameters 后二次 executeUpdate(驱动先发 COM_STMT_RESET)直接崩溃 openGauss 服务端:客户端报 `EOFException: read 0 bytes before connection was unexpectedly lost`,服务端日志戛然而止(无 PANIC、无正常关闭流程),3308 不可达需人工重启;单测可复现(两个工程同构);8.0.33+ 档同一序列正常 | 严重(单次 JDBC 操作即可打崩整个服务端,可被任意客户端触发拒绝服务) | 8.0.28 档避免服务端预编译语句 clearParameters 后复用(新建语句或换 8.0.33+ 驱动);服务端修复后回归 |
234+ 
235+# 6 测试过程评估
236+ 
237+## 6.1 测试策略回顾
238+ 
239+先以 Boot BOM 默认驱动跑通冒烟建立基线,再针对 ORM 高风险环节(方言/元数据/主键/查询渲染)设计专项用例;对发现的转义语义差异,先在 8.0.28 文档基准驱动上复现以排除驱动因素,再下钻到 SQL 层用 gsql 取证定位服务端语义差异;最后以驱动矩阵确认问题归属。
240+ 
241+## 6.2 测试设计评估
242+ 
243+1. **已知差异回归测试方法论**:K1 把"当前已知的差异行为"写成断言——将来服务端行为调整后测试自动失败,提示更新文档、移除 workaround,让测试成为文档的守护者;
244+2. D1 采用手工拉起独立 Spring 容器的写法(SpringApplicationBuilder),避免 @SpringBootTest 在容器异常时把测试直接标 ERROR,从而能观察"启动成功但表未创建"的中间状态——这是捕获 FINDING E 的关键设计;
245+3. 驱动矩阵与 #27 报告共享同一方法论,两个任务结论互相印证;
246+4. 协议命令专项(C1-C9)与 #27 报告 7.1-7.9 同一套设计,覆盖 JDBC 可触发的全部协议命令(COM_QUERY/INIT_DB/PING/STMT_*/SET_OPTION/QUIT),无触发路径命令在 4.1.6 说明原因;C5/C8 将 FINDING F/G 的"当前错误行为"写成回归断言(服务端修复后自动失败提醒更新报告),与 K1/D1 同一方法论。
247+ 
248+## 6.3 测试执行评估
249+ 
250+### 6.3.1 测试执行统计数据
251+ 
252+| 项目 | 数据 |
253+| ---- | ---- |
254+| 用例总数 | 19 |
255+| 驱动档位 | 7(含文档基准 8.0.28 与最新 26.7.0) |
256+| 执行轮次 | 7 档各一轮全量回归(8.0.28 档因 FINDING I 服务端崩溃分两段执行:协议类 9 + 其余 10)+ 若干轮开发调试与单用例复测 |
257+| 总执行用例次 | 19 × 7 = 133(含 8.0.28 档两段) |
258+ 
259+### 6.3.2 测试用例执行结果统计数据
260+ 
261+| 结果 | 数量 |
262+| ---- | ---- |
263+| 通过 | 122(8.0.28 档 12 / 8.0.33~8.4.0 档各 18 / 9.7.0 档 19 / 26.7.0 档 19) |
264+| 失败 | 5(FINDING H 断言失败:8.0.28 档 1 + 8.0.33~8.4.0 档各 1) |
265+| 错误 | 6(FINDING I 服务端崩溃后失联:8.0.28 档 6) |
266+ 
267+# 7 附件
268+ 
269+## 7.1 附件1:相关产物
270+ 
271+| 产物 | 位置 |
272+| ---- | ---- |
273+| 测试代码(19 用例) | 本目录 `src/test/java/org/opengauss/smoke/jpacases/` |
274+| 测试思维导图 | 本目录 `docs/测试思维导图-SpringDataJPA连接openGaussB库.{mm,md,html}` |
275+| 转义语义差异取证材料 | 本报告 7.2 节(gsql 三条取证 SQL 输出,服务端直连复现,不依赖 Java 组件) |
276+| 指导文档 | docs 仓 `docs/zh/mysql_compatibility_developer_guide/springdatajpa_development.md`(轻量版 `docs-lite/zh/...` 同路径) |
277+ 
278+## 7.2 附件2:LIKE escape 转义语义差异 gsql 取证输出(与 Plugin 仓 issue #504 同源)
279+ 
280+> 取证环境:openGauss 7.0.0-RC3(commit fe6f6e4b,debug 编译),B 库 proto_test_db,通过 gsql(postgres 协议 5432)直连复现——不依赖任何 Java 组件,与驱动版本无关。
281+ 
282+```sql
283+proto_test_db=# SELECT '\\';
284+ \\
285+----
286+ \\
287+(1 row)
288+ 
289+proto_test_db=# SELECT 'abc' LIKE 'a%' ESCAPE '\\';
290+ERROR: invalid escape string
291+HINT: Escape string must be empty or one character.
292+ 
293+proto_test_db=# SELECT 'abc' LIKE 'a%' ESCAPE '!';
294+ ?column?
295+----------
296+ t
297+(1 row)
298+```
299+ 
300+三条 SQL 说明:① openGauss B 模式按标准 SQL 解释字符串字面量,`'\\'` 是两个字符的反斜杠(MySQL 反斜杠转义语义下是单字符);② LIKE 的 ESCAPE 子句要求转义符为空或单字符,两字符的 `'\\'` 触发 `invalid escape string`;③ 换用单字符 `!` 作为转义符即可正常执行。Hibernate 6.6 默认给 LIKE 追加 `escape '\\'`,故派生模糊查询(如 `findByNameLike`)在 B 模式下全部报错(该差异的回归用例见 4.1.1 节),JPQL 显式 `escape '!'` 可规避。该差异的根源是 B 模式不支持反斜杠作为字符串转义字符,与社区已记录的已知差异同源(Plugin 仓 issue #504)。
@@ -0,0 +1,123 @@
1+<!DOCTYPE html>
2+<html lang="zh-CN">
3+<head>
4+<meta charset="UTF-8">
5+<title>测试思维导图:SpringDataJPA + MySQL JDBC 连接 openGauss B 库</title>
6+<style>
7+ body { font-family: "Microsoft YaHei", "PingFang SC", sans-serif; background: #f4f6f8; margin: 0; padding: 24px; }
8+ h1 { font-size: 18px; text-align: center; color: #2c3e50; margin: 8px 0 4px; }
9+ p.sub { text-align: center; color: #7f8c8d; font-size: 13px; margin: 0 0 20px; }
10+ ul.map-tree, ul.map-tree ul { list-style: none; }
11+ ul.map-tree { padding-left: 8px; }
12+ ul.map-tree ul { border-left: 2px solid #c8d1da; margin-left: 16px; padding-left: 18px; }
13+ ul.map-tree li { margin: 7px 0; position: relative; }
14+ ul.map-tree ul li::before {
15+ content: ""; position: absolute; left: -18px; top: 11px;
16+ width: 16px; border-top: 2px solid #c8d1da;
17+ }
18+ .root > .label {
19+ display: inline-block; background: #1f3a5f; color: #fff; font-weight: bold;
20+ padding: 10px 18px; border-radius: 22px; font-size: 16px;
21+ box-shadow: 0 3px 8px rgba(31,58,95,.25);
22+ }
23+ .branch > .label {
24+ display: inline-block; color: #fff; font-weight: bold;
25+ padding: 7px 14px; border-radius: 16px; font-size: 14px;
26+ }
27+ .leaf > .label { font-size: 13.5px; color: #34495e; padding: 2px 0; }
28+ .leaf-note > .label { font-size: 13px; color: #b03a2e; background: #fdecea; padding: 2px 8px; border-radius: 6px; }
29+ .b-env > .label { background: #7f8c8d; }
30+ .b-issue > .label { background: #2980b9; }
31+ .b-ddl > .label { background: #c0392b; }
32+ .b-map > .label { background: #27ae60; }
33+ .b-page > .label { background: #e67e22; }
34+ .b-smoke > .label { background: #16a085; }
35+ .b-proto > .label { background: #d35400; }
36+ .b-matrix > .label { background: #2c3e50; }
37+ .b-concl > .label { background: #1e8449; }
38+ .pass { color: #27ae60; font-weight: bold; }
39+ .foot { text-align: center; color: #95a5a6; font-size: 12px; margin-top: 24px; }
40+</style>
41+</head>
42+<body>
43+<h1>测试思维导图:SpringDataJPA + MySQL JDBC 连接 openGauss B 库(Issue #28)</h1>
44+<p class="sub">19 个测试用例(BOM 默认 9.7.0 基线全通过)· 7 档驱动矩阵协议命令分三档 · 转义语义差异(#504)/ FINDING E / F / G / H / I 已记录</p>
45+<ul class="map-tree">
46+ <li class="root"><span class="label">SpringDataJPA + MySQL JDBC × openGauss B 库 测试</span>
47+ <ul>
48+ <li class="branch b-env"><span class="label">测试环境</span>
49+ <ul>
50+ <li class="leaf"><span class="label">openGauss 7.0.0-RC3(fe6f6e4b)B 库 proto_test_db / schema mysql_test_db(协议 3308)</span></li>
51+ <li class="leaf"><span class="label">Spring Boot 3.5.16 + Hibernate 6.6.53 + mysql-connector-j 9.7.0(BOM 默认)</span></li>
52+ <li class="leaf"><span class="label">方言 MySQLDialect(自定义 OpenGaussBMySQLDialect 保留适配点)</span></li>
53+ </ul>
54+ </li>
55+ <li class="branch b-issue"><span class="label">一、已知差异回归(2 用例 <span class="pass">✓</span>)</span>
56+ <ul>
57+ <li class="leaf-note"><span class="label">K1:派生 LIKE(findByNameLike)复现转义语义差异 —— invalid escape string(Hibernate 6.6 默认 escape '\\'(两个反斜杠),B 模式视 '\\' 为 2 字符;根源是 B 模式不支持反斜杠作为字符串转义字符,与 Plugin 仓 issue #504 同源)</span></li>
58+ <li class="leaf"><span class="label">K2:JPQL 显式 escape '!'(searchByName)可用 —— workaround 稳定,与驱动版本无关</span></li>
59+ <li class="leaf"><span class="label">方法论:K1 写成回归断言 —— 服务端行为调整后测试自动失败,提醒移除 workaround 并更新文档</span></li>
60+ </ul>
61+ </li>
62+ <li class="branch b-ddl"><span class="label">二、ddl-auto 专项(2 用例)</span>
63+ <ul>
64+ <li class="leaf-note"><span class="label">D1 FINDING E:ddl-auto=update 容器启动成功但表未创建(静默失效,不建表也不报错)</span></li>
65+ <li class="leaf"><span class="label">根因:默认配置 dolphin.lower_case_table_names=0 下 information_schema 表名大小写敏感(KEYWORDS 大写报不存在,小写可查)→ schema 对比失败</span></li>
66+ <li class="leaf"><span class="label">解法:dolphin.lower_case_table_names=1 并持久化后实测大写 KEYWORDS 可查、Hibernate 元数据探测恢复</span></li>
67+ <li class="leaf"><span class="label">实际影响:应用启动正常,运行时才报 relation does not exist</span></li>
68+ <li class="leaf"><span class="label">D2:ddl-auto=validate 未报错(元数据同样缺失,校验深度存疑,如实表述)</span></li>
69+ </ul>
70+ </li>
71+ <li class="branch b-map"><span class="label">三、类型映射(1 用例 <span class="pass">✓</span>)</span>
72+ <ul>
73+ <li class="leaf"><span class="label">BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 往返一致</span></li>
74+ <li class="leaf"><span class="label">IDENTITY 主键回填(getGeneratedKeys 链路正常)</span></li>
75+ </ul>
76+ </li>
77+ <li class="branch b-page"><span class="label">四、分页与批量(2 用例 <span class="pass">✓</span>)</span>
78+ <ul>
79+ <li class="leaf"><span class="label">P1:PageRequest 多页遍历(LIMIT/OFFSET 与 count 查询正常)</span></li>
80+ <li class="leaf"><span class="label">P2:saveAll(50) 批量主键逐个回填</span></li>
81+ </ul>
82+ </li>
83+ <li class="branch b-smoke"><span class="label">五、冒烟(3 用例 <span class="pass">✓</span>)</span>
84+ <ul>
85+ <li class="leaf"><span class="label">saveAndFind / pagination / transactionRollback(@Transactional 回滚正常)</span></li>
86+ </ul>
87+ </li>
88+ <li class="branch b-proto"><span class="label">六、协议命令专项(9 用例 <span class="pass">✓</span>,评审意见落地,与 #27 报告 7.1-7.9 同设计)</span>
89+ <ul>
90+ <li class="leaf"><span class="label">C1 COM_QUERY / C2 COM_INIT_DB / C3 COM_PING</span></li>
91+ <li class="leaf"><span class="label">C4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE(useServerPrepStmts=true 全生命周期)</span></li>
92+ <li class="leaf"><span class="label">C5 COM_STMT_SEND_LONG_DATA:无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)</span></li>
93+ <li class="leaf-note"><span class="label">FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整</span></li>
94+ <li class="leaf-note"><span class="label">FINDING F:含 0x00 二进制被按 C 字符串截断为前缀(200 字节读回 150;客户端预编译路径无此问题)</span></li>
95+ <li class="leaf"><span class="label">C6 COM_STMT_RESET(clearParameters 后复用,8.0.33 及以上档正常)</span></li>
96+ <li class="leaf-note"><span class="label">FINDING I:8.0.28 档此序列直接崩溃 openGauss 服务端进程(EOFException 断连、可复现 3 次)</span></li>
97+ <li class="leaf"><span class="label">C7 COM_STMT_FETCH:驱动层可用、协议层待确认</span></li>
98+ <li class="leaf"><span class="label">C8 COM_SET_OPTION(allowMultiQueries 多语句)</span></li>
99+ <li class="leaf-note"><span class="label">FINDING G:多语句后续 SELECT 结果集 getMoreResults() 不可得(DML 均落库)</span></li>
100+ <li class="leaf"><span class="label">C9 COM_QUIT;无 JDBC 触发路径的命令在报告 4.1.6 说明</span></li>
101+ </ul>
102+ </li>
103+ <li class="branch b-matrix"><span class="label">驱动版本矩阵(7 档 × 19 用例)</span>
104+ <ul>
105+ <li class="leaf-note"><span class="label">8.0.28(文档基准):FINDING I 崩溃 + FINDING H 丢 8KB,其余 12 通过(含 K1/D1 复现)</span></li>
106+ <li class="leaf"><span class="label">8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:18/19,仅 FINDING H(RESET 正常、服务端不崩)</span></li>
107+ <li class="leaf"><span class="label">9.7.0(Boot 默认)/ 26.7.0(最新):19/19 全部通过</span></li>
108+ <li class="leaf"><span class="label">三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 → <b>核心链路与 K1/D1/FINDING E/F/G 均与驱动版本无关;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本</b></span></li>
109+ </ul>
110+ </li>
111+ <li class="branch b-concl"><span class="label">结论</span>
112+ <ul>
113+ <li class="leaf"><span class="label">JPA 核心链路(类型映射 / 主键回填 / 分页 / 批量 / 事务)可用</span></li>
114+ <li class="leaf"><span class="label">六个问题/差异根源在服务端:① 反斜杠转义语义差异(已知差异,与 #504 同源)② FINDING E 大小写敏感(配置可恢复)③ FINDING F 二进制 0x00 截断 ④ FINDING G 多语句结果结构 ⑤ FINDING H 丢末块 8KB(8.0.28~8.4.0 档)⑥ FINDING I 崩溃(仅 8.0.28 档)</span></li>
115+ <li class="leaf"><span class="label">指导文档建议:LIKE 显式 escape '!';ddl-auto 建议 lower_case_table_names=1(或 none + 手工建表);服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动</span></li>
116+ </ul>
117+ </li>
118+ </ul>
119+ </li>
120+</ul>
121+<p class="foot">测试代码与自测报告见同目录 · 2026-09-04(v1.3)</p>
122+</body>
123+</html>
@@ -0,0 +1,89 @@
1+# 测试思维导图:SpringDataJPA + MySQL JDBC 连接 openGauss B 库(Issue #28)
2+ 
3+> 大纲版思维导图。可视化版见同名 .html,可编辑版见同名 .mm(XMind / FreeMind / MindMaster 均可打开)。
4+ 
5+```text
6+SpringDataJPA + MySQL JDBC 连接 openGauss B 库 测试(19 用例;BOM 默认 9.7.0 基线全通过)
7+│
8+├── 测试环境
9+│ ├── openGauss 7.0.0-RC3(fe6f6e4b,debug)B 兼容库 proto_test_db / schema mysql_test_db
10+│ ├── MySQL 协议端口 3308(enable_dolphin_proto = on)
11+│ └── Spring Boot 3.5.16 + Hibernate 6.6.53 + mysql-connector-j 9.7.0(BOM 默认)
12+│ 方言 MySQLDialect(自定义 OpenGaussBMySQLDialect 保留适配点)
13+│
14+├── 一、已知差异回归(2 用例 ✓)
15+│ ├── K1:派生 LIKE(findByNameLike)复现转义语义差异(#504)
16+│ │ invalid escape string —— Hibernate 6.6 默认追加 escape '\\'
17+│ │ (两个反斜杠;MySQL 中 '\\'=1 字符),B 模式按标准 SQL 把 '\\'
18+│ │ 视为 2 字符;根源是 B 模式不支持反斜杠作为字符串转义字符
19+│ │ (与 Plugin 仓 issue #504 同源)
20+│ ├── K2:JPQL 显式 escape '!'(searchByName)可用 ✓
21+│ │ workaround 稳定,且与驱动版本无关(8.0.28 与 9.7.0 同行为)
22+│ └── 方法论:K1 写成回归断言 —— 服务端行为调整后测试自动失败,
23+│ 提醒移除 workaround 并更新文档
24+│
25+├── 二、ddl-auto 专项(2 用例)
26+│ ├── D1:ddl-auto=update 容器启动成功但表未创建 → FINDING E
27+│ │ HHH000339:information_schema.KEYWORDS 不存在
28+│ │ (默认配置 dolphin.lower_case_table_names=0 下大小写敏感:小写可查)
29+│ │ → schema 对比失败 → update 静默失效(不建表也不报错)
30+│ │ → 实际项目:启动正常,运行时才报 relation does not exist
31+│ │ → 解法:dolphin.lower_case_table_names=1 并持久化后实测大写 KEYWORDS
32+│ │ 可查、Hibernate 元数据探测恢复
33+│ └── D2:ddl-auto=validate 未报错(元数据同样缺失,校验深度存疑,如实表述)
34+│
35+├── 三、类型映射(1 用例 ✓)
36+│ └── M1:BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 往返
37+│ + IDENTITY 主键回填(getGeneratedKeys 链路正常)
38+│
39+├── 四、分页与批量(2 用例 ✓)
40+│ ├── P1:PageRequest 多页遍历(LIMIT/OFFSET 与 count 查询正常)
41+│ └── P2:saveAll(50) 批量主键逐个回填
42+│
43+├── 五、冒烟(3 用例 ✓)
44+│ └── saveAndFind / pagination / transactionRollback(@Transactional 回滚正常)
45+│
46+├── 六、协议命令专项(9 用例 ✓,评审意见落地,与 #27 报告 7.1-7.9 同设计)
47+│ ├── C1 COM_QUERY / C2 COM_INIT_DB / C3 COM_PING ✓
48+│ ├── C4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE
49+│ │ (useServerPrepStmts=true 全生命周期)✓
50+│ ├── C5 COM_STMT_SEND_LONG_DATA(服务端预编译 + 1MB LONGBLOB)
51+│ │ ├── 无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)✓
52+│ │ ├── FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,
53+│ │ │ 丢末块 8KB,MD5 失败;9.7.0/26.7.0 档完整)
54+│ │ └── FINDING F:含 0x00 二进制被按 C 字符串截断为前缀
55+│ │ (200 字节读回 150 = 首个 0x00 位置;客户端预编译路径无此问题)
56+│ ├── C6 COM_STMT_RESET(clearParameters 后复用)✓(8.0.33 及以上档)
57+│ │ └── FINDING I:8.0.28 档此序列直接崩溃 openGauss 服务端进程
58+│ │ (EOFException 断连、3308 不可达、可复现 3 次)
59+│ ├── C7 COM_STMT_FETCH(useCursorFetch + setFetchSize)✓
60+│ │ 驱动层可用、协议层待确认
61+│ ├── C8 COM_SET_OPTION(allowMultiQueries 多语句)
62+│ │ └── FINDING G:后续 SELECT 结果集 getMoreResults() 不可得
63+│ │ (仅首语句结果 + 末 DML update count;DML 均落库)
64+│ └── C9 COM_QUIT ✓;无 JDBC 触发路径的命令在报告 4.1.6 说明
65+│
66+├── 驱动版本矩阵(7 档 × 19 用例)
67+│ ├── 8.0.28(文档基准):FINDING I 崩溃 + FINDING H 丢 8KB,
68+│ │ 其余 12 通过(含 K1/D1 复现)
69+│ ├── 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:18/19(仅 FINDING H,
70+│ │ RESET 正常、服务端不崩)
71+│ ├── 9.7.0(Boot 默认)/ 26.7.0(最新):19/19 全部通过
72+│ └── 三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 →
73+│ 核心链路与 K1/D1/FINDING E/F/G 均与驱动版本无关;
74+│ FINDING H/I 为服务端缺陷、触发条件依赖驱动版本
75+│
76+└── 结论
77+ ├── JPA 核心链路(类型映射 / 主键回填 / 分页 / 批量 / 事务)可用
78+ ├── 六个问题/差异根源在服务端:
79+ │ ① 反斜杠转义语义差异(LIKE escape,已知差异,与 #504 同源)
80+ │ ② FINDING E information_schema 表名大小写敏感(ddl-auto 失效,
81+ │ 配置 lower_case_table_names=1 持久化可恢复)
82+ │ ③ FINDING F SEND_LONG_DATA 二进制 0x00 截断
83+ │ ④ FINDING G 多语句结果返回结构不完整
84+ │ ⑤ FINDING H SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档)
85+ │ ⑥ FINDING I COM_STMT_RESET 崩溃服务端(仅 8.0.28 档)
86+ └── 指导文档建议:LIKE 显式 escape '!';ddl-auto 建议
87+ lower_case_table_names=1(或 none + 手工建表);
88+ 服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动
89+```
@@ -0,0 +1,57 @@
1+<map version="1.0.1">
2+<node TEXT="SpringDataJPA + MySQL JDBC 连接 openGauss B 库 测试(19 用例;BOM 默认 9.7.0 基线全通过)">
3+<node TEXT="测试环境" POSITION="right">
4+<node TEXT="openGauss 7.0.0-RC3(fe6f6e4b)B 库 proto_test_db / schema mysql_test_db" POSITION="right"/>
5+<node TEXT="MySQL 协议端口 3308(enable_dolphin_proto = on)" POSITION="right"/>
6+<node TEXT="Spring Boot 3.5.16 + Hibernate 6.6.53 + mysql-connector-j 9.7.0" POSITION="right"/>
7+<node TEXT="方言 MySQLDialect(自定义 OpenGaussBMySQLDialect 保留适配点)" POSITION="right"/>
8+</node>
9+<node TEXT="一、已知差异回归(2 用例,符合预期)" POSITION="right">
10+<node TEXT="K1:派生 LIKE 复现转义语义差异 —— invalid escape string(Hibernate 6.6 默认 escape '\\'(两个反斜杠),B 模式视 '\\' 为 2 字符;与 Plugin 仓 issue #504 同源)" POSITION="right"/>
11+<node TEXT="K2:JPQL 显式 escape '!'(searchByName)可用,workaround 稳定" POSITION="right"/>
12+<node TEXT="方法论:K1 写成回归断言 —— 服务端行为调整后自动失败,提醒更新文档" POSITION="right"/>
13+</node>
14+<node TEXT="二、ddl-auto 专项(2 用例)" POSITION="right">
15+<node TEXT="D1 FINDING E:ddl-auto=update 容器启动成功但表未创建(静默失效)" POSITION="right"/>
16+<node TEXT="根因:默认配置 lower_case_table_names=0 下 information_schema 表名大小写敏感(KEYWORDS 大写报不存在,小写可查)→ schema 对比失败" POSITION="right"/>
17+<node TEXT="解法:dolphin.lower_case_table_names=1 并持久化后实测大写 KEYWORDS 可查、Hibernate 元数据探测恢复" POSITION="right"/>
18+<node TEXT="实际影响:启动正常,运行时才报 relation does not exist" POSITION="right"/>
19+<node TEXT="D2:ddl-auto=validate 未报错(校验深度存疑,如实表述)" POSITION="right"/>
20+</node>
21+<node TEXT="三、类型映射(1 用例,通过)" POSITION="right">
22+<node TEXT="BigDecimal / LocalDate / LocalDateTime / Boolean↔TINYINT(1) / String 往返一致" POSITION="right"/>
23+<node TEXT="IDENTITY 主键回填(getGeneratedKeys 链路正常)" POSITION="right"/>
24+</node>
25+<node TEXT="四、分页与批量(2 用例,通过)" POSITION="right">
26+<node TEXT="P1:PageRequest 多页遍历(LIMIT/OFFSET 与 count 查询正常)" POSITION="right"/>
27+<node TEXT="P2:saveAll(50) 批量主键逐个回填" POSITION="right"/>
28+</node>
29+<node TEXT="五、冒烟(3 用例,通过)" POSITION="right">
30+<node TEXT="saveAndFind / pagination / transactionRollback(@Transactional 回滚正常)" POSITION="right"/>
31+</node>
32+<node TEXT="六、协议命令专项(9 用例,评审意见落地,与 #27 报告 7.1-7.9 同设计)" POSITION="right">
33+<node TEXT="C1 COM_QUERY / C2 COM_INIT_DB / C3 COM_PING" POSITION="right"/>
34+<node TEXT="C4 COM_STMT_PREPARE / EXECUTE / CLOSE(useServerPrepStmts=true 全生命周期)" POSITION="right"/>
35+<node TEXT="C5 COM_STMT_SEND_LONG_DATA:无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)" POSITION="right"/>
36+<node TEXT="FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整" POSITION="right"/>
37+<node TEXT="FINDING F:含 0x00 二进制被按 C 字符串截断为前缀(200 字节读回 150;客户端预编译路径无此问题)" POSITION="right"/>
38+<node TEXT="C6 COM_STMT_RESET(clearParameters 后复用,8.0.33 及以上档正常)" POSITION="right"/>
39+<node TEXT="FINDING I:8.0.28 档 COM_STMT_RESET 直接崩溃 openGauss 服务端进程(EOFException 断连、可复现 3 次)" POSITION="right"/>
40+<node TEXT="C7 COM_STMT_FETCH:驱动层可用、协议层待确认" POSITION="right"/>
41+<node TEXT="C8 COM_SET_OPTION(allowMultiQueries)" POSITION="right"/>
42+<node TEXT="FINDING G:多语句后续 SELECT 结果集 getMoreResults() 不可得(DML 均落库)" POSITION="right"/>
43+<node TEXT="C9 COM_QUIT;无 JDBC 触发路径的命令在报告 4.1.6 说明" POSITION="right"/>
44+</node>
45+<node TEXT="驱动版本矩阵(7 档 × 19 用例)" POSITION="right">
46+<node TEXT="8.0.28(文档基准):FINDING I 崩溃 + FINDING H 丢 8KB,其余 12 通过(含 K1/D1 复现)" POSITION="right"/>
47+<node TEXT="8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:18/19,仅 FINDING H(RESET 正常、服务端不崩)" POSITION="right"/>
48+<node TEXT="9.7.0(Boot 默认)/ 26.7.0(最新):19/19 全部通过" POSITION="right"/>
49+<node TEXT="三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 → 核心链路与 K1/D1/FINDING E/F/G 与驱动版本无关;FINDING H/I 为服务端缺陷、触发条件依赖驱动版本" POSITION="right"/>
50+</node>
51+<node TEXT="结论" POSITION="right">
52+<node TEXT="JPA 核心链路(类型映射 / 主键回填 / 分页 / 批量 / 事务)可用" POSITION="right"/>
53+<node TEXT="问题/差异根源在服务端:反斜杠转义语义差异(已知差异,与 #504 同源)/ FINDING E 大小写敏感(配置可恢复)/ FINDING F 二进制 0x00 截断 / FINDING G 多语句结果结构 / FINDING H 丢末块 8KB(8.0.28~8.4.0 档)/ FINDING I 崩溃(仅 8.0.28 档)" POSITION="right"/>
54+<node TEXT="指导文档建议:LIKE 显式 escape '!';ddl-auto 建议 lower_case_table_names=1(或 none + 手工建表);服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动" POSITION="right"/>
55+</node>
56+</node>
57+</map>
@@ -0,0 +1,129 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<project xmlns="http://maven.apache.org/POM/4.0.0"
3+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+ <modelVersion>4.0.0</modelVersion>
6+ 
7+ <parent>
8+ <groupId>org.springframework.boot</groupId>
9+ <artifactId>spring-boot-starter-parent</artifactId>
10+ <version>3.5.16</version>
11+ <relativePath/>
12+ </parent>
13+ 
14+ <groupId>org.opengauss</groupId>
15+ <artifactId>springdatajpa-connect-opengauss-b</artifactId>
16+ <version>1.0.0</version>
17+ <name>springdatajpa-connect-opengauss-b</name>
18+ <description>Spring Data JPA 连接 openGauss B 兼容库 — Issue #28</description>
19+ 
20+ <properties>
21+ <java.version>17</java.version>
22+ </properties>
23+ 
24+ <dependencies>
25+ <dependency>
26+ <groupId>org.springframework.boot</groupId>
27+ <artifactId>spring-boot-starter-data-jpa</artifactId>
28+ </dependency>
29+ <dependency>
30+ <groupId>org.springframework.boot</groupId>
31+ <artifactId>spring-boot-starter-test</artifactId>
32+ <scope>test</scope>
33+ </dependency>
34+ </dependencies>
35+ 
36+ <!-- 驱动扫测 profile:与 springboot-connect-opengauss-b 保持同一套档位 -->
37+ <profiles>
38+ <profile>
39+ <id>driver-boot-default</id>
40+ <activation>
41+ <activeByDefault>true</activeByDefault>
42+ </activation>
43+ <dependencies>
44+ <dependency>
45+ <groupId>com.mysql</groupId>
46+ <artifactId>mysql-connector-j</artifactId>
47+ <scope>runtime</scope>
48+ </dependency>
49+ </dependencies>
50+ </profile>
51+ <profile>
52+ <id>driver-8.0.28</id>
53+ <dependencies>
54+ <dependency>
55+ <groupId>mysql</groupId>
56+ <artifactId>mysql-connector-java</artifactId>
57+ <version>8.0.28</version>
58+ <scope>runtime</scope>
59+ </dependency>
60+ </dependencies>
61+ </profile>
62+ <profile>
63+ <id>driver-8.0.33</id>
64+ <dependencies>
65+ <dependency>
66+ <groupId>com.mysql</groupId>
67+ <artifactId>mysql-connector-j</artifactId>
68+ <version>8.0.33</version>
69+ <scope>runtime</scope>
70+ </dependency>
71+ </dependencies>
72+ </profile>
73+ <profile>
74+ <id>driver-8.1.0</id>
75+ <dependencies>
76+ <dependency>
77+ <groupId>com.mysql</groupId>
78+ <artifactId>mysql-connector-j</artifactId>
79+ <version>8.1.0</version>
80+ <scope>runtime</scope>
81+ </dependency>
82+ </dependencies>
83+ </profile>
84+ <profile>
85+ <id>driver-8.3.0</id>
86+ <dependencies>
87+ <dependency>
88+ <groupId>com.mysql</groupId>
89+ <artifactId>mysql-connector-j</artifactId>
90+ <version>8.3.0</version>
91+ <scope>runtime</scope>
92+ </dependency>
93+ </dependencies>
94+ </profile>
95+ <profile>
96+ <id>driver-8.4.0</id>
97+ <dependencies>
98+ <dependency>
99+ <groupId>com.mysql</groupId>
100+ <artifactId>mysql-connector-j</artifactId>
101+ <version>8.4.0</version>
102+ <scope>runtime</scope>
103+ </dependency>
104+ </dependencies>
105+ </profile>
106+ <profile>
107+ <id>driver-9.7.0</id>
108+ <dependencies>
109+ <dependency>
110+ <groupId>com.mysql</groupId>
111+ <artifactId>mysql-connector-j</artifactId>
112+ <version>9.7.0</version>
113+ <scope>runtime</scope>
114+ </dependency>
115+ </dependencies>
116+ </profile>
117+ <profile>
118+ <id>driver-26.7.0</id>
119+ <dependencies>
120+ <dependency>
121+ <groupId>com.mysql</groupId>
122+ <artifactId>mysql-connector-j</artifactId>
123+ <version>26.7.0</version>
124+ <scope>runtime</scope>
125+ </dependency>
126+ </dependencies>
127+ </profile>
128+ </profiles>
129+</project>
@@ -0,0 +1,25 @@
1+package org.opengauss.smoke;
2+ 
3+import org.hibernate.dialect.MySQLDialect;
4+ 
5+/**
6+ * openGauss B 兼容库方言(应用经 MySQL JDBC 驱动 + dolphin 协议连接)。
7+ *
8+ * openGauss B 模式解析器按标准 SQL 解释字符串字面量(无反斜杠转义,
9+ * '\\' 是两个字符),与 MySQL 不同,故在此声明"无反斜杠转义"语义。
10+ *
11+ * 已知兼容性问题:Hibernate 6.6 默认给 LIKE 谓词追加 escape '\\'
12+ * (两个反斜杠),B 模式解析器因 '\\' 是两字符而报 "invalid escape string"
13+ * (与驱动版本无关,8.0.28 与 9.7.0 表现一致)。该差异的根源是 B 模式不
14+ * 支持反斜杠作为字符串转义字符(与社区已记录的已知差异同源,参见 Plugin
15+ * 仓 issue #504)。实测 6.6.53 的 LIKE 渲染不读取本 isNoBackslashEscapesEnabled
16+ * 开关,规避方式是在 JPQL 中显式指定单字符转义符:like ?1 escape '!'
17+ * (见 UserRepository.searchByName)。
18+ */
19+public class OpenGaussBMySQLDialect extends MySQLDialect {
20+ 
21+ @Override
22+ public boolean isNoBackslashEscapesEnabled() {
23+ return true;
24+ }
25+}
@@ -0,0 +1,52 @@
1+package org.opengauss.smoke;
2+ 
3+import org.springframework.boot.CommandLineRunner;
4+import org.springframework.boot.SpringApplication;
5+import org.springframework.boot.autoconfigure.SpringBootApplication;
6+import org.springframework.data.domain.Page;
7+import org.springframework.data.domain.PageRequest;
8+import org.springframework.stereotype.Component;
9+ 
10+/**
11+ * JPA 冒烟程序:验证 Hibernate 6 在 MySQL 协议链路上的行为。
12+ * 重点观察:启动元数据查询(show-sql 可见)、IDENTITY 主键回填、分页 SQL。
13+ */
14+@SpringBootApplication
15+public class SmokeApplication {
16+ 
17+ public static void main(String[] args) {
18+ SpringApplication.run(SmokeApplication.class, args);
19+ }
20+ 
21+ @Component
22+ public static class SmokeRunner implements CommandLineRunner {
23+ 
24+ private final UserRepository repo;
25+ 
26+ public SmokeRunner(UserRepository repo) {
27+ this.repo = repo;
28+ }
29+ 
30+ @Override
31+ public void run(String... args) {
32+ // 1. 查询
33+ long count = repo.count();
34+ System.out.println("[SMOKE] 连接成功,User 总数 = " + count);
35+ 
36+ // 2. 保存:验证 IDENTITY 主键回填(getGeneratedKeys)
37+ User saved = repo.save(new User("smoke", 99));
38+ System.out.println("[SMOKE] 保存成功,主键回填 id = " + saved.getId());
39+ 
40+ // 3. 分页:Hibernate 生成 LIMIT/OFFSET
41+ Page<User> page = repo.findAll(PageRequest.of(0, 2));
42+ System.out.println("[SMOKE] 分页成功,本页 " + page.getContent().size()
43+ + " 条,共 " + page.getTotalElements() + " 条");
44+ 
45+ // 4. 模糊查询(显式 escape '!' 规避 B 模式反斜杠转义问题)+ 删除
46+ Page<User> found = repo.searchByName("%smoke%", PageRequest.of(0, 5));
47+ System.out.println("[SMOKE] 模糊查询成功,命中 " + found.getTotalElements() + " 条");
48+ repo.delete(saved);
49+ System.out.println("[SMOKE] 删除成功,JPA 全链路通过");
50+ }
51+ }
52+}
@@ -0,0 +1,21 @@
1+package org.opengauss.smoke;
2+ 
3+import org.springframework.stereotype.Service;
4+import org.springframework.transaction.annotation.Transactional;
5+ 
6+/** 事务行为验证用服务。 */
7+@Service
8+public class TxService {
9+ 
10+ private final UserRepository repo;
11+ 
12+ public TxService(UserRepository repo) {
13+ this.repo = repo;
14+ }
15+ 
16+ @Transactional
17+ public void saveThenThrow(String name, int age) {
18+ repo.save(new User(name, age));
19+ throw new RuntimeException("事务回滚验证");
20+ }
21+}
@@ -0,0 +1,63 @@
1+package org.opengauss.smoke;
2+ 
3+import jakarta.persistence.Entity;
4+import jakarta.persistence.GeneratedValue;
5+import jakarta.persistence.GenerationType;
6+import jakarta.persistence.Id;
7+import jakarta.persistence.Table;
8+ 
9+/**
10+ * 业务表 `user` 的实体类。
11+ * 注意:Spring Boot 3.x / Hibernate 6 使用 jakarta.* 包(Boot 2.x 是 javax.*)。
12+ * user 在 MySQL 中是保留字,Hibernate 6 默认自动加反引号(auto_quote_keyword)。
13+ */
14+@Entity
15+@Table(name = "user")
16+public class User {
17+ 
18+ /** IDENTITY 策略对应 MySQL 的 AUTO_INCREMENT,回填依赖 JDBC getGeneratedKeys。 */
19+ @Id
20+ @GeneratedValue(strategy = GenerationType.IDENTITY)
21+ private Integer id;
22+ 
23+ private String name;
24+ 
25+ private Integer age;
26+ 
27+ public User() {
28+ }
29+ 
30+ public User(String name, Integer age) {
31+ this.name = name;
32+ this.age = age;
33+ }
34+ 
35+ public Integer getId() {
36+ return id;
37+ }
38+ 
39+ public void setId(Integer id) {
40+ this.id = id;
41+ }
42+ 
43+ public String getName() {
44+ return name;
45+ }
46+ 
47+ public void setName(String name) {
48+ this.name = name;
49+ }
50+ 
51+ public Integer getAge() {
52+ return age;
53+ }
54+ 
55+ public void setAge(Integer age) {
56+ this.age = age;
57+ }
58+ 
59+ @Override
60+ public String toString() {
61+ return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
62+ }
63+}
@@ -0,0 +1,25 @@
1+package org.opengauss.smoke;
2+ 
3+import org.springframework.data.domain.Page;
4+import org.springframework.data.domain.Pageable;
5+import org.springframework.data.jpa.repository.JpaRepository;
6+import org.springframework.data.jpa.repository.Query;
7+ 
8+/** JpaRepository 自带 CRUD 与 findAll(Pageable)(分页生成 LIMIT/OFFSET)。 */
9+public interface UserRepository extends JpaRepository<User, Integer> {
10+ 
11+ /**
12+ * 派生 LIKE 查询:Hibernate 6.6 默认在所有 LIKE 谓词后追加 escape '\\'
13+ * (两个反斜杠),B 模式按标准 SQL 把 '\\' 视为 2 个字符,报
14+ * invalid escape string(转义语义差异;根源是 B 模式不支持反斜杠作为字符串
15+ * 转义字符,与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。
16+ * 保留此方法作为"已知差异"回归用例。
17+ */
18+ Page<User> findByNameLike(String name, Pageable pageable);
19+ 
20+ /**
21+ * 显式指定非反斜杠转义符(!)的 LIKE 查询,规避上述问题。
22+ */
23+ @Query("select u from User u where u.name like ?1 escape '!'")
24+ Page<User> searchByName(String pattern, Pageable pageable);
25+}
@@ -0,0 +1,14 @@
1+spring:
2+ datasource:
3+ url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3308}/${DB_NAME:mysql_test_db}?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8
4+ username: ${DB_USER:mysql_test_db}
5+ password: ${DB_PASSWORD:ChangeMe_123}
6+ driver-class-name: com.mysql.cj.jdbc.Driver
7+ jpa:
8+ # 表由 SQL 脚本创建,不让 Hibernate 建表/改表;ddl-auto 行为是后续专项测试点
9+ hibernate:
10+ ddl-auto: none
11+ # 走 MySQL 协议。自定义方言声明 B 模式"无反斜杠转义"语义,见 OpenGaussBMySQLDialect。
12+ # LIKE 查询的规避:JPQL 显式 escape '!'(B 模式不认 '\\' 单字符转义,见 UserRepository)
13+ database-platform: org.opengauss.smoke.OpenGaussBMySQLDialect
14+ show-sql: true
@@ -0,0 +1,54 @@
1+package org.opengauss.smoke;
2+ 
3+import org.junit.jupiter.api.Test;
4+import org.springframework.beans.factory.annotation.Autowired;
5+import org.springframework.boot.test.context.SpringBootTest;
6+import org.springframework.data.domain.Page;
7+import org.springframework.data.domain.PageRequest;
8+ 
9+import static org.junit.jupiter.api.Assertions.assertEquals;
10+import static org.junit.jupiter.api.Assertions.assertNotNull;
11+ 
12+/** JPA 冒烟用例:需真实 openGauss B 库环境,与 driver profile 配合做版本扫测。 */
13+@SpringBootTest
14+class SmokeTest {
15+ 
16+ @Autowired
17+ private UserRepository repo;
18+ 
19+ @Autowired
20+ private TxService txService;
21+ 
22+ @Test
23+ void saveAndFind() {
24+ User saved = repo.save(new User("t_jpa", 1));
25+ // IDENTITY 主键回填
26+ assertNotNull(saved.getId(), "save 后应回填自增主键");
27+ 
28+ User found = repo.findById(saved.getId()).orElseThrow();
29+ assertEquals("t_jpa", found.getName());
30+ repo.delete(saved);
31+ }
32+ 
33+ @Test
34+ void pagination() {
35+ Page<User> page = repo.findAll(PageRequest.of(0, 2));
36+ assertEquals(2, page.getContent().size(), "每页应返回 2 条(LIMIT 2)");
37+ repo.searchByName("%t_%", PageRequest.of(0, 5));
38+ }
39+ 
40+ @Test
41+ void transactionRollback() {
42+ try {
43+ txService.saveThenThrow("t_tx", 1);
44+ } catch (RuntimeException expected) {
45+ // 预期异常:触发回滚
46+ }
47+ long count = repo.count();
48+ repo.findAll().forEach(u -> {
49+ if ("t_tx".equals(u.getName())) {
50+ throw new AssertionError("事务应已回滚,不应存在 t_tx 记录");
51+ }
52+ });
53+ }
54+}
@@ -0,0 +1,100 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import org.junit.jupiter.api.DisplayName;
4+import org.junit.jupiter.api.Test;
5+import org.opengauss.smoke.SmokeApplication;
6+import org.springframework.boot.builder.SpringApplicationBuilder;
7+import org.springframework.context.ConfigurableApplicationContext;
8+ 
9+import java.sql.Connection;
10+import java.sql.PreparedStatement;
11+import java.sql.ResultSet;
12+import java.sql.SQLException;
13+ 
14+import static org.junit.jupiter.api.Assertions.assertEquals;
15+import static org.junit.jupiter.api.Assertions.assertTrue;
16+import static org.opengauss.smoke.jpacases.JpaDbUtil.*;
17+ 
18+/**
19+ * ddl-auto 行为专项。
20+ * 手工拉起独立 Spring 容器(避免 @SpringBootTest 在容器启动失败时把测试直接标 ERROR),
21+ * 分别验证 update / validate 两种模式在 B 模式下的实际行为。
22+ */
23+class DdlAutoTest {
24+ 
25+ private static boolean tableExists(String table) throws SQLException {
26+ try (Connection c = raw();
27+ PreparedStatement ps = c.prepareStatement("SELECT COUNT(*) FROM " + table);
28+ ResultSet rs = ps.executeQuery()) {
29+ rs.next();
30+ return true;
31+ }
32+ }
33+ 
34+ @Test
35+ @DisplayName("D1 ddl-auto=update:观察 Hibernate 是否自动创建缺失表 t_jpa_ddl")
36+ void ddlAutoUpdate() throws SQLException {
37+ // 先确保目标表不存在(从干净状态出发)
38+ try (Connection c = raw()) {
39+ exec(c, "DROP TABLE IF EXISTS t_jpa_ddl");
40+ }
41+ boolean contextOk = true;
42+ String failure = null;
43+ try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(SmokeApplication.class)
44+ .properties("spring.jpa.hibernate.ddl-auto=update")
45+ .run()) {
46+ log("D1 ddl-auto=update 容器启动成功");
47+ } catch (Exception e) {
48+ contextOk = false;
49+ failure = rootMessage(e);
50+ log("D1 FINDING: ddl-auto=update 容器启动失败: %s", failure);
51+ }
52+ boolean created;
53+ try {
54+ created = tableExists("t_jpa_ddl");
55+ } catch (SQLException e) {
56+ created = false;
57+ }
58+ if (contextOk && !created) {
59+ log("D1 FINDING: 容器启动成功但 t_jpa_ddl 未被创建。原因:连接元数据获取失败"
60+ + "(information_schema.KEYWORDS 不存在,见日志 HHH000339),"
61+ + "Hibernate 无法做 schema 对比 -> ddl-auto=update 静默失效(不建表也不报错)。"
62+ + "实际项目影响:应用启动正常,运行时才报 relation does not exist");
63+ } else if (created) {
64+ log("D1 t_jpa_ddl 已被 Hibernate 自动创建(ddl-auto=update 可用,可能服务端已修复元数据)");
65+ // 新表上做一次真实读写,确认生成的表结构可用(IDENTITY 回填)
66+ try (Connection c = raw()) {
67+ exec(c, "INSERT INTO t_jpa_ddl (name) VALUES ('ddl_ok')");
68+ }
69+ try (Connection c = raw();
70+ PreparedStatement ps = c.prepareStatement("SELECT name FROM t_jpa_ddl WHERE name='ddl_ok'");
71+ ResultSet rs = ps.executeQuery()) {
72+ assertTrue(rs.next());
73+ assertEquals("ddl_ok", rs.getString(1));
74+ log("D1 生成的表上读写正常");
75+ }
76+ }
77+ // 两种结果都不做硬断言:当前 B 模式实际行为是"不建表"(作为 FINDING 记录),
78+ // 将来服务端修复 information_schema 后,本用例会自动观察到 created=true
79+ }
80+ 
81+ @Test
82+ @DisplayName("D2 ddl-auto=validate:启动校验行为(可能受 information_schema 缺失影响)")
83+ void ddlAutoValidate() {
84+ try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(SmokeApplication.class)
85+ .properties("spring.jpa.hibernate.ddl-auto=validate")
86+ .run()) {
87+ log("D2 ddl-auto=validate 通过:Hibernate 元数据校验未报错");
88+ } catch (Exception e) {
89+ log("D2 FINDING: ddl-auto=validate 启动失败: %s", rootMessage(e));
90+ }
91+ }
92+ 
93+ private static String rootMessage(Throwable t) {
94+ Throwable cur = t;
95+ while (cur.getCause() != null) {
96+ cur = cur.getCause();
97+ }
98+ return cur.getMessage();
99+ }
100+}
@@ -0,0 +1,45 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import jakarta.persistence.Entity;
4+import jakarta.persistence.GeneratedValue;
5+import jakarta.persistence.GenerationType;
6+import jakarta.persistence.Id;
7+import jakarta.persistence.Table;
8+ 
9+/**
10+ * ddl-auto 行为测试专用实体,对应表 t_jpa_ddl(表本身不存在,由 Hibernate 生成)。
11+ * 结构刻意简单(id + name),便于观察 Hibernate 生成的建表 SQL 在 B 模式下的表现。
12+ */
13+@Entity
14+@Table(name = "t_jpa_ddl")
15+public class DdlEntity {
16+ 
17+ @Id
18+ @GeneratedValue(strategy = GenerationType.IDENTITY)
19+ private Integer id;
20+ 
21+ private String name;
22+ 
23+ public DdlEntity() {
24+ }
25+ 
26+ public DdlEntity(String name) {
27+ this.name = name;
28+ }
29+ 
30+ public Integer getId() {
31+ return id;
32+ }
33+ 
34+ public void setId(Integer id) {
35+ this.id = id;
36+ }
37+ 
38+ public String getName() {
39+ return name;
40+ }
41+ 
42+ public void setName(String name) {
43+ this.name = name;
44+ }
45+}
@@ -0,0 +1,58 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import java.sql.Connection;
4+import java.sql.DriverManager;
5+import java.sql.SQLException;
6+import java.sql.Statement;
7+ 
8+/** JPA 用例公共工具:原生 JDBC 连接(建测试表用)与统一日志。 */
9+public final class JpaDbUtil {
10+ 
11+ public static final String HOST = env("DB_HOST", "127.0.0.1");
12+ public static final String PORT = env("DB_PORT", "3308");
13+ public static final String DB = env("DB_NAME", "mysql_test_db");
14+ public static final String USER = env("DB_USER", "mysql_test_db");
15+ public static final String PASSWORD = env("DB_PASSWORD", "ChangeMe_123");
16+ 
17+ private JpaDbUtil() {
18+ }
19+ 
20+ public static String url() {
21+ return "jdbc:mysql://" + HOST + ":" + PORT + "/" + DB
22+ + "?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8";
23+ }
24+ 
25+ /** 原生连接(不走连接池),用于建表/DDL 与元数据检查。 */
26+ public static Connection raw() throws SQLException {
27+ return DriverManager.getConnection(url(), USER, PASSWORD);
28+ }
29+ 
30+ /** 拼 MySQL JDBC URL;extraParams 形如 "useServerPrepStmts=true&...",null 表示不带额外参数。 */
31+ public static String url(String extraParams) {
32+ String base = url();
33+ return extraParams == null || extraParams.isEmpty() ? base : base + "&" + extraParams;
34+ }
35+ 
36+ /** 原生连接(带驱动参数),用于协议命令专项。 */
37+ public static Connection raw(String extraParams) throws SQLException {
38+ return DriverManager.getConnection(url(extraParams), USER, PASSWORD);
39+ }
40+ 
41+ public static void exec(Connection conn, String... sqls) throws SQLException {
42+ try (Statement st = conn.createStatement()) {
43+ for (String sql : sqls) {
44+ st.execute(sql);
45+ }
46+ }
47+ }
48+ 
49+ /** 统一日志前缀:[CASE] 开头的行会被整理进自测报告。 */
50+ public static void log(String fmt, Object... args) {
51+ System.out.println("[CASE] " + String.format(fmt, args));
52+ }
53+ 
54+ private static String env(String key, String def) {
55+ String v = System.getenv(key);
56+ return (v == null || v.isEmpty()) ? def : v;
57+ }
58+}
@@ -0,0 +1,99 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import jakarta.persistence.Column;
4+import jakarta.persistence.Entity;
5+import jakarta.persistence.GeneratedValue;
6+import jakarta.persistence.GenerationType;
7+import jakarta.persistence.Id;
8+import jakarta.persistence.Table;
9+ 
10+import java.math.BigDecimal;
11+import java.time.LocalDate;
12+import java.time.LocalDateTime;
13+ 
14+/**
15+ * JPA 类型映射测试实体,对应表 t_jpa_map(由 MappingTest 的 @BeforeAll 用 SQL 创建)。
16+ * 覆盖 JPA/Hibernate 6 常用映射:BigDecimal / LocalDate / LocalDateTime / Boolean / String。
17+ */
18+@Entity
19+@Table(name = "t_jpa_map")
20+public class JpaMapEntity {
21+ 
22+ @Id
23+ @GeneratedValue(strategy = GenerationType.IDENTITY)
24+ private Integer id;
25+ 
26+ @Column(name = "n_dec")
27+ private BigDecimal dec;
28+ 
29+ @Column(name = "n_date")
30+ private LocalDate date;
31+ 
32+ @Column(name = "n_datetime")
33+ private LocalDateTime datetime;
34+ 
35+ @Column(name = "n_bool")
36+ private Boolean bool;
37+ 
38+ @Column(name = "n_name")
39+ private String name;
40+ 
41+ public JpaMapEntity() {
42+ }
43+ 
44+ public JpaMapEntity(BigDecimal dec, LocalDate date, LocalDateTime datetime, Boolean bool, String name) {
45+ this.dec = dec;
46+ this.date = date;
47+ this.datetime = datetime;
48+ this.bool = bool;
49+ this.name = name;
50+ }
51+ 
52+ public Integer getId() {
53+ return id;
54+ }
55+ 
56+ public void setId(Integer id) {
57+ this.id = id;
58+ }
59+ 
60+ public BigDecimal getDec() {
61+ return dec;
62+ }
63+ 
64+ public void setDec(BigDecimal dec) {
65+ this.dec = dec;
66+ }
67+ 
68+ public LocalDate getDate() {
69+ return date;
70+ }
71+ 
72+ public void setDate(LocalDate date) {
73+ this.date = date;
74+ }
75+ 
76+ public LocalDateTime getDatetime() {
77+ return datetime;
78+ }
79+ 
80+ public void setDatetime(LocalDateTime datetime) {
81+ this.datetime = datetime;
82+ }
83+ 
84+ public Boolean getBool() {
85+ return bool;
86+ }
87+ 
88+ public void setBool(Boolean bool) {
89+ this.bool = bool;
90+ }
91+ 
92+ public String getName() {
93+ return name;
94+ }
95+ 
96+ public void setName(String name) {
97+ this.name = name;
98+ }
99+}
@@ -0,0 +1,7 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import org.springframework.data.jpa.repository.JpaRepository;
4+ 
5+/** t_jpa_map 的仓储(MappingTest 用)。 */
6+public interface JpaMapRepository extends JpaRepository<JpaMapEntity, Integer> {
7+}
@@ -0,0 +1,61 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import org.junit.jupiter.api.DisplayName;
4+import org.junit.jupiter.api.Test;
5+import org.opengauss.smoke.User;
6+import org.opengauss.smoke.UserRepository;
7+import org.springframework.beans.factory.annotation.Autowired;
8+import org.springframework.boot.test.context.SpringBootTest;
9+import org.springframework.data.domain.Page;
10+import org.springframework.data.domain.PageRequest;
11+import org.springframework.orm.jpa.JpaSystemException;
12+ 
13+import static org.junit.jupiter.api.Assertions.assertTrue;
14+import static org.opengauss.smoke.jpacases.JpaDbUtil.log;
15+ 
16+/**
17+ * 已知差异回归用例(LIKE escape 转义语义差异)。
18+ * Hibernate 6.6 派生 LIKE 查询默认生成 escape '\\'(两个反斜杠),B 模式
19+ * 按标准 SQL 把 '\\' 当两字符,报 invalid escape string(与驱动版本无关,
20+ * gsql 已取证)。该差异的根源是 B 模式不支持反斜杠作为字符串转义字符
21+ * (与社区已记录的已知差异同源,参见 Plugin 仓 issue #504)。
22+ *
23+ * 这类用例的写法:把"当前已知的差异行为"写成断言。将来服务端行为调整后,
24+ * 第一个用例会失败,提醒我们移除 workaround 并更新文档——这是回归测试的标准用法。
25+ */
26+@SpringBootTest
27+class KnownIssueTest {
28+ 
29+ @Autowired
30+ private UserRepository repo;
31+ 
32+ @Test
33+ @DisplayName("K1 派生 findByNameLike 报 invalid escape string(转义语义差异现状断言)")
34+ void derivedLikeFailsAsExpected() {
35+ try {
36+ Page<User> page = repo.findByNameLike("%t_%", PageRequest.of(0, 5));
37+ log("K1 注意:派生 LIKE 不再报错(命中 %d 条)!服务端可能已调整转义语义,"
38+ + "应移除 searchByName 的 escape '!' workaround 并更新文档", page.getTotalElements());
39+ } catch (JpaSystemException e) {
40+ String msg = rootMessage(e);
41+ log("K1 已知差异复现(符合当前预期): %s", msg);
42+ assertTrue(msg.contains("invalid escape string"),
43+ "应报 invalid escape string,实际: " + msg);
44+ }
45+ }
46+ 
47+ @Test
48+ @DisplayName("K2 显式 escape '!' 的 searchByName 正常工作")
49+ void explicitEscapeWorks() {
50+ Page<User> page = repo.searchByName("%t_%", PageRequest.of(0, 5));
51+ log("K2 searchByName(escape '!')命中 %d 条,规避方案有效", page.getTotalElements());
52+ }
53+ 
54+ private static String rootMessage(Throwable t) {
55+ Throwable cur = t;
56+ while (cur.getCause() != null) {
57+ cur = cur.getCause();
58+ }
59+ return cur.getMessage();
60+ }
61+}
@@ -0,0 +1,67 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import org.junit.jupiter.api.BeforeAll;
4+import org.junit.jupiter.api.DisplayName;
5+import org.junit.jupiter.api.Test;
6+import org.springframework.beans.factory.annotation.Autowired;
7+import org.springframework.boot.test.context.SpringBootTest;
8+ 
9+import java.math.BigDecimal;
10+import java.sql.Connection;
11+import java.sql.SQLException;
12+import java.time.LocalDate;
13+import java.time.LocalDateTime;
14+ 
15+import static org.junit.jupiter.api.Assertions.assertEquals;
16+import static org.junit.jupiter.api.Assertions.assertNotNull;
17+import static org.opengauss.smoke.jpacases.JpaDbUtil.*;
18+ 
19+/**
20+ * JPA 类型映射:BigDecimal / LocalDate / LocalDateTime / Boolean / String
21+ * 实体-表往返(save → findById 值一致),IDENTITY 主键回填。
22+ */
23+@SpringBootTest
24+class MappingTest {
25+ 
26+ @Autowired
27+ private JpaMapRepository repo;
28+ 
29+ @BeforeAll
30+ static void createTable() throws SQLException {
31+ try (Connection c = raw()) {
32+ exec(c,
33+ "DROP TABLE IF EXISTS t_jpa_map",
34+ "CREATE TABLE t_jpa_map ("
35+ + " id INT AUTO_INCREMENT PRIMARY KEY,"
36+ + " n_dec DECIMAL(10,2),"
37+ + " n_date DATE,"
38+ + " n_datetime DATETIME,"
39+ + " n_bool TINYINT(1),"
40+ + " n_name VARCHAR(50)"
41+ + ")");
42+ }
43+ log("M0 建表 t_jpa_map 成功");
44+ }
45+ 
46+ @Test
47+ @DisplayName("M1 五种类型映射往返 + IDENTITY 回填")
48+ void roundtrip() {
49+ BigDecimal dec = new BigDecimal("3.14");
50+ LocalDate date = LocalDate.of(2026, 8, 28);
51+ LocalDateTime datetime = LocalDateTime.of(2026, 8, 28, 12, 34, 56);
52+ JpaMapEntity saved = repo.save(new JpaMapEntity(dec, date, datetime, Boolean.TRUE, "jpa_map"));
53+ 
54+ assertNotNull(saved.getId(), "save 后应回填自增主键");
55+ log("M1 保存成功,IDENTITY 回填 id=%d", saved.getId());
56+ 
57+ JpaMapEntity found = repo.findById(saved.getId()).orElseThrow();
58+ assertEquals(dec, found.getDec(), "DECIMAL 往返");
59+ assertEquals(date, found.getDate(), "DATE 往返");
60+ assertEquals(datetime, found.getDatetime(), "DATETIME 往返");
61+ assertEquals(Boolean.TRUE, found.getBool(), "TINYINT(1) ↔ Boolean 往返");
62+ assertEquals("jpa_map", found.getName(), "VARCHAR 往返");
63+ log("M1 五种类型映射往返一致:DECIMAL/DATE/DATETIME/Boolean/String");
64+ 
65+ repo.delete(found);
66+ }
67+}
@@ -0,0 +1,70 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import org.junit.jupiter.api.DisplayName;
4+import org.junit.jupiter.api.Test;
5+import org.opengauss.smoke.User;
6+import org.opengauss.smoke.UserRepository;
7+import org.springframework.beans.factory.annotation.Autowired;
8+import org.springframework.boot.test.context.SpringBootTest;
9+import org.springframework.data.domain.Page;
10+import org.springframework.data.domain.PageRequest;
11+ 
12+import java.util.ArrayList;
13+import java.util.List;
14+ 
15+import static org.junit.jupiter.api.Assertions.assertEquals;
16+import static org.junit.jupiter.api.Assertions.assertTrue;
17+import static org.opengauss.smoke.jpacases.JpaDbUtil.log;
18+ 
19+/**
20+ * 分页(LIMIT/OFFSET 生成)与批量保存。
21+ * 清理用 searchByName(escape '!'),刻意避开转义语义差异(#504)的派生 LIKE。
22+ */
23+@SpringBootTest
24+class PaginationBatchTest {
25+ 
26+ @Autowired
27+ private UserRepository repo;
28+ 
29+ @Test
30+ @DisplayName("P1 分页:多页遍历(Hibernate 生成 LIMIT/OFFSET + count)")
31+ void pagination() {
32+ long total = repo.count();
33+ assertTrue(total >= 3, "user 表至少应有 3 条初始数据");
34+ 
35+ int size = 2;
36+ long pages = (total + size - 1) / size;
37+ int seen = 0;
38+ for (int i = 0; i < pages; i++) {
39+ Page<User> page = repo.findAll(PageRequest.of(i, size));
40+ seen += page.getContent().size();
41+ }
42+ assertEquals(total, seen, "逐页遍历应覆盖全部记录");
43+ log("P1 分页遍历 OK:共 %d 条 / 页大小 2 / %d 页(LIMIT/OFFSET 与 count 查询均正常)", total, pages);
44+ }
45+ 
46+ @Test
47+ @DisplayName("P2 saveAll 批量保存 50 条(含 IDENTITY 逐个回填)")
48+ void saveAll() {
49+ List<User> users = new ArrayList<>();
50+ for (int i = 1; i <= 50; i++) {
51+ users.add(new User("t_jpa_batch_" + i, i));
52+ }
53+ long before = repo.count();
54+ repo.saveAll(users);
55+ long after = repo.count();
56+ assertEquals(before + 50, after, "saveAll 应新增 50 条");
57+ 
58+ // 抽查回填的主键均非空
59+ for (User u : users) {
60+ assertTrue(u.getId() != null, "批量保存后主键应回填");
61+ }
62+ log("P2 saveAll(50) 成功:count %d -> %d,主键全部回填", before, after);
63+ 
64+ // 清理:用显式 escape '!' 的查询(避开转义语义差异)
65+ List<User> toDelete = repo.searchByName("%t_jpa_batch_%", PageRequest.of(0, 100)).getContent();
66+ repo.deleteAll(toDelete);
67+ assertEquals(before, repo.count(), "清理后应恢复原行数");
68+ log("P2 清理完成,恢复 %d 行", before);
69+ }
70+}
@@ -0,0 +1,275 @@
1+package org.opengauss.smoke.jpacases;
2+ 
3+import org.junit.jupiter.api.BeforeAll;
4+import org.junit.jupiter.api.DisplayName;
5+import org.junit.jupiter.api.Test;
6+ 
7+import java.io.ByteArrayInputStream;
8+import java.security.MessageDigest;
9+import java.sql.Connection;
10+import java.sql.PreparedStatement;
11+import java.sql.ResultSet;
12+import java.sql.SQLException;
13+import java.sql.Statement;
14+import java.util.Arrays;
15+ 
16+import static org.junit.jupiter.api.Assertions.assertEquals;
17+import static org.junit.jupiter.api.Assertions.assertTrue;
18+import static org.opengauss.smoke.jpacases.JpaDbUtil.*;
19+ 
20+/**
21+ * 协议命令专项(评审意见:COM_STMT_PREPARE / COM_STMT_SEND_LONG_DATA /
22+ * COM_STMT_EXECUTE 最好覆盖到每个 command)。
23+ * 与 #27 报告(springboot-connect-opengauss-b)7.1-7.9 同一套设计,按"JDBC 是否有触发路径"逐条实测:
24+ * C1 COM_QUERY(普通 Statement,文本协议)
25+ * C2 COM_INIT_DB(连接建立携带 database;setCatalog + useLocalSessionState=false)
26+ * C3 COM_PING(isValid)
27+ * C4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE(服务端预编译)
28+ * C5 COM_STMT_SEND_LONG_DATA(服务端预编译 + 1MB LONGBLOB 流式写入)
29+ * C6 COM_STMT_RESET(clearParameters 后复用同一预编译语句)
30+ * C7 COM_STMT_FETCH(useCursorFetch + setFetchSize)
31+ * C8 COM_SET_OPTION(allowMultiQueries 多语句)
32+ * C9 COM_QUIT(close)
33+ * COM_FIELD_LIST / COM_PROCESS_INFO / COM_STATISTICS / COM_PROCESS_KILL / COM_CHANGE_USER
34+ * 等命令 JDBC 驱动无触发路径(不提供对应 API),在自测报告 4.1.6 说明,不在此处用例化。
35+ */
36+class ProtocolCommandTest {
37+ 
38+ @BeforeAll
39+ static void createData() throws SQLException {
40+ try (Connection c = raw()) {
41+ exec(c,
42+ "DROP TABLE IF EXISTS jpa_cmd",
43+ "CREATE TABLE jpa_cmd (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), data LONGBLOB)");
44+ try (PreparedStatement ps = c.prepareStatement(
45+ "INSERT INTO jpa_cmd (name) VALUES (?)")) {
46+ for (int i = 1; i <= 500; i++) {
47+ ps.setString(1, "cmd_" + i);
48+ ps.addBatch();
49+ }
50+ ps.executeBatch();
51+ }
52+ log("C0 准备协议命令测试数据(jpa_cmd 500 行,含 LONGBLOB 列)");
53+ }
54+ }
55+ 
56+ private static int count(Connection c) throws SQLException {
57+ try (Statement st = c.createStatement();
58+ ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM jpa_cmd")) {
59+ rs.next();
60+ return rs.getInt(1);
61+ }
62+ }
63+ 
64+ @Test
65+ @DisplayName("C1 COM_QUERY:普通 Statement 执行 SQL(文本协议)")
66+ void comQuery() throws SQLException {
67+ try (Connection c = raw()) {
68+ int before = count(c);
69+ try (Statement st = c.createStatement()) {
70+ st.executeUpdate("INSERT INTO jpa_cmd (name) VALUES ('query_a')");
71+ st.executeUpdate("INSERT INTO jpa_cmd (name) VALUES ('query_b')");
72+ }
73+ int after = count(c);
74+ log("C1 COM_QUERY 文本协议插入/查询正常:%d -> %d 行", before, after);
75+ assertEquals(before + 2, after);
76+ }
77+ }
78+ 
79+ @Test
80+ @DisplayName("C2 COM_INIT_DB:连接建立携带 database + setCatalog(useLocalSessionState=false)")
81+ void comInitDb() throws SQLException {
82+ // 连接建立阶段:url 指定 database,驱动握手时即发出 COM_INIT_DB;
83+ // useLocalSessionState=false 关闭本地状态缓存,setCatalog 按协议重新确认 database。
84+ try (Connection c = raw("useLocalSessionState=false")) {
85+ assertEquals(DB, c.getCatalog());
86+ c.setCatalog(DB);
87+ assertTrue(count(c) >= 500, "切换 catalog 后查询应正常");
88+ log("C2 COM_INIT_DB 连接初始化/切换 database 正常:catalog=%s", c.getCatalog());
89+ }
90+ }
91+ 
92+ @Test
93+ @DisplayName("C3 COM_PING:Connection.isValid")
94+ void comPing() throws SQLException {
95+ try (Connection c = raw()) {
96+ boolean ok = c.isValid(2);
97+ log("C3 COM_PING 心跳正常:isValid=%s", ok);
98+ assertTrue(ok);
99+ }
100+ }
101+ 
102+ @Test
103+ @DisplayName("C4 COM_STMT_PREPARE/EXECUTE/CLOSE:服务端预编译语句全生命周期")
104+ void comStmtPrepareExecuteClose() throws SQLException {
105+ // cachePrepStmts=false 确保每次 prepareStatement 真实发出 COM_STMT_PREPARE
106+ try (Connection c = raw("useServerPrepStmts=true&cachePrepStmts=false")) {
107+ try (PreparedStatement ps = c.prepareStatement(
108+ "INSERT INTO jpa_cmd (name) VALUES (?)")) {
109+ ps.setString(1, "prep_c4");
110+ assertEquals(1, ps.executeUpdate());
111+ } // 语句 close -> COM_STMT_CLOSE
112+ try (PreparedStatement ps = c.prepareStatement(
113+ "SELECT name FROM jpa_cmd WHERE name = ?")) {
114+ ps.setString(1, "prep_c4");
115+ try (ResultSet rs = ps.executeQuery()) {
116+ assertTrue(rs.next(), "服务端预编译语句执行后应查回已插入行");
117+ assertEquals("prep_c4", rs.getString(1));
118+ }
119+ }
120+ log("C4 COM_STMT_PREPARE/EXECUTE/CLOSE 服务端预编译插入+查询往返正常");
121+ }
122+ }
123+ 
124+ @Test
125+ @DisplayName("C5 COM_STMT_SEND_LONG_DATA:服务端预编译 + 1MB LONGBLOB 流式写入")
126+ void comStmtSendLongData() throws SQLException {
127+ try (Connection c = raw("useServerPrepStmts=true")) {
128+ // ① 链路基线:无零字节 1MB(全 0x41)应完整往返(MD5 一致)。
129+ byte[] blob = new byte[1024 * 1024];
130+ Arrays.fill(blob, (byte) 0x41);
131+ String expectMd5 = md5(blob);
132+ try (PreparedStatement ps = c.prepareStatement(
133+ "INSERT INTO jpa_cmd (name, data) VALUES (?, ?)")) {
134+ ps.setString(1, "long_c5");
135+ ps.setBlob(2, new ByteArrayInputStream(blob));
136+ assertEquals(1, ps.executeUpdate());
137+ }
138+ byte[] read;
139+ try (PreparedStatement ps = c.prepareStatement(
140+ "SELECT data FROM jpa_cmd WHERE name = ?")) {
141+ ps.setString(1, "long_c5");
142+ try (ResultSet rs = ps.executeQuery()) {
143+ assertTrue(rs.next());
144+ read = rs.getBytes(1);
145+ }
146+ }
147+ log("C5a COM_STMT_SEND_LONG_DATA 1MB 无零字节 LONGBLOB 流式写入并读回:%d -> %d 字节,MD5 %s",
148+ blob.length, read.length, expectMd5.equals(md5(read)) ? "一致" : "不一致");
149+ assertEquals(blob.length, read.length);
150+ assertEquals(expectMd5, md5(read));
151+ 
152+ // ② FINDING F 回归断言:二进制内容遇 0x00 被服务端按 C 字符串截断为前缀
153+ // (与 #27 报告 FINDING F 同源;MySQL 原生完整存储)。
154+ // 服务端修复后本断言自动失败,提醒移除规避说明并更新自测报告。
155+ byte[] mixed = new byte[200];
156+ Arrays.fill(mixed, (byte) 0x41);
157+ mixed[150] = 0x00;
158+ Arrays.fill(mixed, 151, mixed.length, (byte) 0x42);
159+ try (PreparedStatement ps = c.prepareStatement(
160+ "INSERT INTO jpa_cmd (name, data) VALUES (?, ?)")) {
161+ ps.setString(1, "long_c5_mixed");
162+ ps.setBlob(2, new ByteArrayInputStream(mixed));
163+ assertEquals(1, ps.executeUpdate());
164+ }
165+ byte[] mixedRead;
166+ try (PreparedStatement ps = c.prepareStatement(
167+ "SELECT data FROM jpa_cmd WHERE name = ?")) {
168+ ps.setString(1, "long_c5_mixed");
169+ try (ResultSet rs = ps.executeQuery()) {
170+ assertTrue(rs.next());
171+ mixedRead = rs.getBytes(1);
172+ }
173+ }
174+ log("C5b FINDING F:含 0x00 的 200 字节读回 %d 字节(=第一个 0x00 前的前缀长度,MySQL 原生应回 200)",
175+ mixedRead.length);
176+ assertEquals(150, mixedRead.length,
177+ "FINDING F 回归断言:dolphin 遇 0x00 截断为前缀;服务端修复后应回 200 字节");
178+ }
179+ }
180+ 
181+ @Test
182+ @DisplayName("C6 COM_STMT_RESET:clearParameters 后复用同一预编译语句")
183+ void comStmtReset() throws SQLException {
184+ try (Connection c = raw("useServerPrepStmts=true")) {
185+ int before = count(c);
186+ try (PreparedStatement ps = c.prepareStatement(
187+ "INSERT INTO jpa_cmd (name) VALUES (?)")) {
188+ ps.setString(1, "reset_a");
189+ assertEquals(1, ps.executeUpdate());
190+ ps.clearParameters(); // 参数置空,下次执行时驱动发 COM_STMT_RESET
191+ ps.setString(1, "reset_b");
192+ assertEquals(1, ps.executeUpdate());
193+ }
194+ int after = count(c);
195+ log("C6 COM_STMT_RESET clearParameters 后复用预编译语句正常:%d -> %d 行", before, after);
196+ assertEquals(before + 2, after);
197+ }
198+ }
199+ 
200+ @Test
201+ @DisplayName("C7 COM_STMT_FETCH:useCursorFetch + setFetchSize")
202+ void comStmtFetch() throws SQLException {
203+ try (Connection c = raw("useCursorFetch=true&useServerPrepStmts=true")) {
204+ int n;
205+ try (PreparedStatement ps = c.prepareStatement(
206+ "SELECT id, name FROM jpa_cmd ORDER BY id")) {
207+ ps.setFetchSize(100);
208+ try (ResultSet rs = ps.executeQuery()) {
209+ n = 0;
210+ while (rs.next()) {
211+ n++;
212+ }
213+ }
214+ }
215+ log("C7 COM_STMT_FETCH 游标拉取无报错,读取 %d 行。协议层是否真逐批返回需服务端日志佐证"
216+ + "(同 #27 报告 6.1 口径,报告中按'驱动层可用、协议层待确认'记录)", n);
217+ assertTrue(n >= 500, "应至少读回准备阶段写入的 500 行");
218+ }
219+ }
220+ 
221+ @Test
222+ @DisplayName("C8 COM_SET_OPTION:allowMultiQueries=true 多语句执行")
223+ void comSetOption() throws SQLException {
224+ try (Connection c = raw("allowMultiQueries=true")) {
225+ int before = count(c);
226+ int cnt = -1;
227+ boolean hasResults;
228+ try (Statement st = c.createStatement()) {
229+ hasResults = st.execute("INSERT INTO jpa_cmd (name) VALUES ('mq_1'); "
230+ + "INSERT INTO jpa_cmd (name) VALUES ('mq_2'); "
231+ + "SELECT COUNT(*) FROM jpa_cmd");
232+ // FINDING G:dolphin 多语句结果返回结构不完整——execute() 只反映首语句
233+ // 结果,getMoreResults() 取不到后续语句的结果集(与 #27 报告 FINDING G 同源;
234+ // MySQL 原生按语句逐个返回全部结果)。服务端修复后 more=true 且 cnt 能读到
235+ // COUNT(*),本断言自动失败提醒更新报告。
236+ hasResults = st.getMoreResults();
237+ if (hasResults) {
238+ try (ResultSet rs = st.getResultSet()) {
239+ if (rs.next()) {
240+ cnt = rs.getInt(1);
241+ }
242+ }
243+ }
244+ }
245+ int after = count(c);
246+ log("C8 COM_SET_OPTION 多语句执行生效:%d -> %d 行(后续 SELECT 结果集经 getMoreResults "
247+ + "不可得,见 FINDING G)", before, after);
248+ assertEquals(before + 2, after, "多语句中的 DML 应全部生效");
249+ assertEquals(-1, cnt,
250+ "FINDING G 回归断言:dolphin 当前不回传后续 SELECT 结果集;服务端修复后 cnt 应=落库行数");
251+ }
252+ }
253+ 
254+ @Test
255+ @DisplayName("C9 COM_QUIT:Connection.close")
256+ void comQuit() throws SQLException {
257+ Connection c = raw();
258+ c.close();
259+ assertTrue(c.isClosed());
260+ log("C9 COM_QUIT 连接关闭正常(isClosed=true)");
261+ }
262+ 
263+ private static String md5(byte[] data) {
264+ try {
265+ byte[] digest = MessageDigest.getInstance("MD5").digest(data);
266+ StringBuilder sb = new StringBuilder();
267+ for (byte b : digest) {
268+ sb.append(String.format("%02x", b));
269+ }
270+ return sb.toString();
271+ } catch (Exception e) {
272+ throw new IllegalStateException(e);
273+ }
274+ }
275+}