已开启
【实习#27】新增 Spring Boot 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #110
【实习#27】新增 Spring Boot 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #110
已开启
wsrym创建于 5 天前
18 个文件变更+2194-0
@@ -0,0 +1,106 @@
1+# Spring Boot (JDBC) 连接 openGauss B 兼容库
2+ 
3+对应任务:Issue #27 — openGauss B 库 B 协议适配 MySQL Java 框架 Spring Boot。
4+ 
5+使用 Spring Boot 3.x 配合 MySQL JDBC 驱动连接 openGauss B 兼容模式数据库的示例工程,包含七维度 37 个测试用例、8 档驱动版本矩阵,以及测试思维导图与自测报告。
6+ 
7+## 目录结构
8+ 
9+```
10+springboot-connect-opengauss-b
11+├── pom.xml # 含 8 档驱动 profile(版本矩阵用)
12+├── README.md
13+├── docs
14+│ ├── 测试思维导图-SpringBoot连接openGaussB库.{mm,md,html}
15+│ └── 基于SpringBoot框架的openGauss数据库测试.md # 自测报告
16+└── src
17+ ├── main/java/org/opengauss/smoke # 示例应用(SmokeApplication)
18+ └── test/java/org/opengauss/cases # 七维度测试套件(37 用例)
19+```
20+ 
21+## 前置条件
22+ 
23+1. 开发机:JDK 17+、Maven 3.6+(Spring Boot 3.x 要求 Java 17)
24+2. openGauss 服务端(openEuler + openGauss 7.0.0,开启 MySQL 协议兼容):
25+ - `enable_dolphin_proto = on``dolphin_server_port = 3308`
26+ - 已创建 B 库 `proto_test_db`、schema `mysql_test_db`、业务表 `` `user` ``
27+ - 用户 `mysql_test_db``set_native_password` 并赋权,pg_hba 已放行
28+3. 网络:开发机可访问 `<VM_IP>:3308`(防火墙放行)
29+ 
30+环境搭建步骤详见 docs 仓《[Spring Boot 连接示例](https://docs.opengauss.org/zh/docs/latest/mysql_compatibility_developer_guide/springboot_development.html)》。
31+ 
32+## 配置
33+ 
34+通过环境变量覆盖连接信息:
35+ 
36+| 变量 | 默认值 | 说明 |
37+|---|---|---|
38+| DB_HOST | 127.0.0.1 | openGauss 所在机器的 IP |
39+| DB_PORT | 3308 | dolphin 协议端口(不是 5432 原生口) |
40+| DB_NAME | mysql_test_db | B 库下 schema(等价 MySQL database) |
41+| DB_USER / DB_PASSWORD | mysql_test_db / ChangeMe_123 | MySQL 认证用户 |
42+ 
43+## 运行
44+ 
45+```bash
46+# 运行全部测试(37 用例)
47+mvn test
48+ 
49+# 运行示例主程序
50+mvn spring-boot:run
51+```
52+ 
53+## 测试内容(七维度 37 用例)
54+ 
55+| 维度 | 用例数 | 覆盖点 |
56+|---|---|---|
57+| 连接与认证 | 4 | 正常连接/错误密码/错误用户/5432 原生口拒绝 + 元数据信息 |
58+| 类型映射 | 8 | 9 种类型往返 + tinyInt1isBit/transformedBitIsBoolean/zeroDateTimeBehavior/useOldAliasMetadataBehavior/useaffectedrows/allowMultiQueries 六个文档参数 |
59+| DML 批量 | 3 | addBatch 1000 行 / JdbcTemplate / rewriteBatchedStatements 提速对比 |
60+| 事务 | 5 | TransactionTemplate 提交回滚 / 手动事务 / SAVEPOINT / 隔离级别 |
61+| 连接池 | 2 | HikariCP 配置生效 / CONNECTION_ID() 验证复用 |
62+| 已知限制 | 3 | 游标 / Statement.setFetchSize / 存储过程 |
63+| 协议命令 | 9 | COM_QUERY/INIT_DB/PING/STMT_PREPARE/STMT_EXECUTE/STMT_CLOSE/STMT_SEND_LONG_DATA/STMT_RESET/STMT_FETCH/SET_OPTION/QUIT(评审意见落地) |
64+ 
65+## 驱动版本矩阵(8 档 × 37 用例,协议命令行为分三档)
66+ 
67+| 驱动版本 | 坐标 | 结果 |
68+|---|---|---|
69+| 8.0.28(文档基准) | mysql:mysql-connector-java(旧坐标) | 协议类 1 失败 + 6 错误(FINDING I 崩溃服务端 + FINDING H 丢 8KB),其余 30 通过 |
70+| 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0 | com.mysql:mysql-connector-j(新坐标) | 36/37 × 4(仅 FINDING H:SEND_LONG_DATA 丢末块 8KB) |
71+| 9.7.0(Boot 3.5.16 BOM 默认) | 同上 | 37/37 |
72+| 26.7.0(最新) | 同上 | 37/37 |
73+ 
74+切换档位重跑:
75+ 
76+```bash
77+mvn test -Pdriver-8.0.28 # 文档基准(旧坐标;注意 FINDING I 会打崩服务端,需重启)
78+mvn test -Pdriver-8.0.33
79+mvn test -Pdriver-8.1.0
80+mvn test -Pdriver-8.3.0
81+mvn test -Pdriver-8.4.0
82+mvn test -Pdriver-9.7.0
83+mvn test -Pdriver-26.7.0
84+```
85+ 
86+> 基础功能(CRUD/事务/批量/类型映射/连接池)8 档逐项一致;协议命令专项按驱动版本分三档:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常(FINDING H/I,缺陷在服务端)。详见 docs/ 自测报告 4.2 节。
87+ 
88+## 已知限制与 FINDING
89+ 
90+| 项 | 说明 |
91+|---|---|
92+| FINDING A | `getTables` 报错 information_schema.TABLES 不存在(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感:大写报错、小写可查;设置 =1 并持久化后实测恢复) |
93+| FINDING B | 零日期由 `dolphin.sql_mode``no_zero_date` 控制:默认开启写入被拒,去掉后可存储;存储后 zeroDateTimeBehavior 三档读取均生效(convertToNull→null / exception→抛错 / round→0001-01-01) |
94+| FINDING C(已改写为正向确认) | useOldAliasMetadataBehavior 在表列别名场景参数生效(true 时 getColumnName 返回别名);表达式别名两种设置一致是文档规定的预期行为 |
95+| FINDING D | SERIALIZABLE 被映射为 REPEATABLE_READ |
96+| FINDING F | COM_STMT_SEND_LONG_DATA(服务端预编译)写入含 0x00 的二进制被按 C 字符串截断为前缀(无零字节 1MB 完整往返 MD5 一致;客户端预编译路径无此问题) |
97+| FINDING G | allowMultiQueries 多语句后续 SELECT 结果集经 getMoreResults() 不可得(仅首语句结果 + 末 DML update count;DML 均落库) |
98+| FINDING H | 服务端预编译 + SEND_LONG_DATA 写入 1MB 全 0x41 数据时,8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整往返 |
99+| FINDING I | 8.0.28 档 clearParameters 后二次执行(COM_STMT_RESET)可复现崩溃 openGauss 服务端进程(EOFException 断连、3308 不可达、需重启);8.0.33+ 档正常 |
100+| 游标 | 驱动层无报错,协议层是否真游标待确认 |
101+| 存储过程 | prepareCall 报错 PARAMETERS 不存在(默认配置大小写敏感所致,`lower_case_table_names=1` 后大写可查;文档已声明不支持) |
102+ 
103+完整分析见 `docs/基于SpringBoot框架的openGauss数据库测试.md`
104+ 
105+> Maven 依赖拉取失败时,在 `~/.m2/settings.xml` 配置阿里云镜像:
106+> `<mirror><id>aliyun</id><mirrorOf>central</mirrorOf><url>https://maven.aliyun.com/repository/central</url></mirror>`
@@ -0,0 +1,322 @@
1+# 基于SpringBoot框架的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 Boot + MySQL JDBC 的兼容性测试 | lele-2026 |
11+| 2026-08-29 | 1.1 | 用例扩充至 28(补测 useaffectedrows / allowMultiQueries);FINDING C 改写为参数生效的正向确认;修正 transformedBitIsBoolean 默认值表述 | lele-2026 |
12+| 2026-08-30 | 1.2 | 用例统计口径修正(六维度 25 + 冒烟 3);驱动矩阵明确 8 档 = BOM 默认档 + 7 个显式版本 | lele-2026 |
13+| 2026-09-04 | 1.3 | 协议命令专项 9 用例实测结果回填(新增 FINDING F/G);FINDING A/B 定位修正(information_schema 大小写敏感为默认配置行为、`dolphin.lower_case_table_names=1` 可解;零日期由 `dolphin.sql_mode` 的 `no_zero_date` 控制、三档 zeroDateTimeBehavior 均生效),与 docs 仓指导文档口径对齐 | lele-2026 |
14+| 2026-09-04 | 1.4 | 驱动矩阵全量补测:协议命令行为按驱动版本分三档(新增 FINDING H/I)——SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档)、COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档,可复现 3 次);9.7.0/26.7.0 档 37/37 全绿;矩阵结论由"行为逐项一致"改写为三档行为 | lele-2026 |
15+| 2026-09-04 | 1.5 | 删除 7.1 遗留问题列表(内部跟踪事项不再在公开报告列出,各 FINDING 的现状与规避已完整记录于 3.3/5.2;与 #28 报告同步处理);附件重编号;环境表移除内网 IP;附件表移除"原始日志备存"行 | lele-2026 |
16+ 
17+[TOC]
18+ 
19+**关键词**
20+ 
21+Spring Boot,MySQL JDBC,openGauss 数据库,B 兼容模式,dolphin 插件,兼容性测试
22+ 
23+**Abstract 摘要**
24+ 
25+本测试验证使用 MySQL JDBC 驱动(mysql-connector-j)连接 openGauss B 兼容模式数据库,在 Spring Boot 3.x 框架下的兼容性。测试覆盖连接与认证、类型映射、DML 批量、事务、连接池、已知限制、协议命令七个维度 34 个用例与 Spring Boot 冒烟 3 个用例共 37 个,并扫描 8 档驱动(BOM 默认档 + 7 个显式版本,8.0.28 ~ 26.7.0)验证版本边界。结果显示:Spring Boot 3.x 配合 MySQL JDBC 驱动在 openGauss B 模式下基础 CRUD、事务、批量、类型映射、连接池场景在 8 档驱动下均可正常工作;协议命令专项存在两个与驱动版本相关的服务端缺陷(8.0.28 档 COM_STMT_RESET 崩溃服务端、8.0.28~8.4.0 档 SEND_LONG_DATA 丢末块 8KB,9.7.0 起正常);共记录 7 项 FINDING(A:information_schema 表名大小写敏感,默认配置所致、`dolphin.lower_case_table_names=1` 可解;B:零日期由 `dolphin.sql_mode` 的 `no_zero_date` 控制;D:隔离级别映射;F:COM_STMT_SEND_LONG_DATA 二进制参数遇 0x00 截断;G:多语句结果返回结构不完整;H:SEND_LONG_DATA 1MB 丢末块 8KB,8.0.28~8.4.0 档;I:COM_STMT_RESET 崩溃服务端进程,仅 8.0.28 档),并针对官方协议兼容文档列出的 6 个 JDBC 参数逐一实测(3 项验证生效,3 项行为与文档声明一致,其中 zeroDateTimeBehavior 三档读取行为实测均生效),已在报告中给出规避措施。
26+ 
27+**缩略语清单:**
28+ 
29+| 缩略语 | 英文全名 | 中文解释 |
30+| ------ | --------------------------- | -------------- |
31+| JDBC | Java Database Connectivity | Java 数据库连接 |
32+| CRUD | Create/Read/Update/Delete | 增删改查 |
33+| DDL | Data Definition Language | 数据定义语言 |
34+| DML | Data Manipulation Language | 数据操作语言 |
35+| BOM | Bill of Materials | 依赖物料清单 |
36+ 
37+***
38+ 
39+# 1 概述
40+ 
41+本报告针对 Spring Boot 框架配合 MySQL JDBC 驱动与 openGauss B 兼容模式数据库的兼容性测试活动进行总结。openGauss 的 dolphin 插件实现了 MySQL 协议兼容,用户在服务端开启 `enable_dolphin_proto` 后,可通过 MySQL 的 JDBC 驱动直接连接 openGauss B 库。
42+ 
43+本测试的测试对象为 Spring Boot 3.5.16 框架(内嵌 mysql-connector-j 9.7.0 驱动)与 openGauss 数据库(B 兼容模式)的交互,另以 mysql-connector-java 8.0.28 作为官方文档声明的基线版本对照。
44+ 
45+# 2 测试版本说明
46+ 
47+## 2.1 测试版本信息
48+ 
49+### 2.1.1 被测版本
50+ 
51+| 版本名称 | 软件包名称 | 测试起始时间 | 测试结束时间 | 测试人员 |
52+| ---------- | ----------------------------- | ------------ | ------------ | -------- |
53+| Spring Boot | spring-boot-starter-jdbc 3.5.16 | 2026-08-28 | 2026-08-29 | lele-2026 |
54+| mysql-connector-j | 9.7.0(Boot BOM 默认) | 2026-08-28 | 2026-08-29 | lele-2026 |
55+| openGauss | openGauss 7.0.0-RC3(源码编译,commit fe6f6e4b) | 2026-08-28 | 2026-08-29 | lele-2026 |
56+ 
57+> 说明:任务要求基于 openGauss 7.0.0-LTS,实测环境为 7.0.0-RC3(与 issue 开发指导文档同版本),二者在 MySQL 协议兼容行为上应一致;正式版发布后建议回归。
58+>
59+> 说明:被测二进制基于 7.0.0-RC3(commit fe6f6e4b)的本地源码工作副本编译:dolphin 插件为本地 contrib/dolphin 副本,包含此前 round() 支持 blob 任务的本地改动(已核验二进制中存在 round(binary/blob) 系列函数签名),contrib/timescaledb 未参与编译。上述差异均不在本测试覆盖的 MySQL 协议 / JDBC / 类型映射 / 事务路径上,不影响测试结论。
60+ 
61+### 2.1.2 配套测试的版本
62+ 
63+| 版本名称 | 配套版本 | 版本说明 |
64+| -------- | --------------- | ----------------------------------------------- |
65+| JDK | 17.0.15(Liberica) | Spring Boot 3.x 要求 JDK 17+ |
66+| Maven | 3.9.14 | 构建工具 |
67+| 驱动矩阵 | 8 档(BOM 默认档 + 7 个显式版本) | BOM 默认档(Boot 3.5.16 管理,解析为 9.7.0)/ 8.0.28(旧坐标)/ 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0 / 9.7.0(显式)/ 26.7.0,用于版本边界验证 |
68+ 
69+## 2.2 测试环境描述
70+ 
71+### 2.2.1 环境硬件信息
72+ 
73+| 环境信息 | 硬件配置信息 | 备注 |
74+| -------- | ------------------------------------------------------------ | ----------------- |
75+| 开发机 | OS:Windows 11<br />JDK 17.0.15<br />Maven 3.9.14 | 测试代码执行环境 |
76+| 虚拟机 | OS:openEuler 20.03 LTS(x86_64)<br />openGauss 源码编译安装(debug) | 数据库环境 |
77+ 
78+### 2.2.2 虚拟化平台
79+ 
80+| 虚拟化平台 | 版本说明 |
81+| ------------------ | -------- |
82+| VMware Workstation Pro | NAT 网络模式,Windows 开发机可直连虚拟机 |
83+ 
84+### 2.2.3 OS版本
85+ 
86+| 操作系统 | OS版本 | 版本说明 |
87+| --------- | ----------- | ------------------------ |
88+| openEuler | 20.03 (LTS) | x86_64 版本 |
89+ 
90+### 2.2.4 数据库环境配置
91+ 
92+| 配置项 | 值 |
93+| ----------------------------- | ----------------------------------------- |
94+| 协议开关 | `enable_dolphin_proto = on` |
95+| MySQL 协议端口 | `dolphin_server_port = 3308` |
96+| 默认数据库 | `dolphin.default_database_name = 'proto_test_db'` |
97+| 测试数据库 | `proto_test_db`(DBCOMPATIBILITY 'B') |
98+| 测试用户 | `mysql_test_db`(已执行 `set_native_password` 原生密码认证) |
99+| 客户端接入认证 | pg_hba.conf:`host all mysql_test_db 0.0.0.0/0 sha256` |
100+| 防火墙 | firewalld 放行 3308/tcp |
101+| 测试表 | `` `user` ``(id INT AUTO_INCREMENT PK, name VARCHAR(50), age INT),预置 3 行;t_cmd(协议命令专项,含 LONGBLOB 列,预置 500 行) |
102+ 
103+# 3 版本概要测试结论、关键风险和规避措施
104+ 
105+## 3.1 测试结论总结
106+ 
107+本测试共设计 37 个测试用例:七个维度 34 个(连接与认证 4、类型映射与文档参数 8、DML 批量 3、事务 5、连接池 2、已知限制专项 3、协议命令专项 9)+ Spring Boot 冒烟 3 个。**37 个用例全部通过**("通过"= 行为与断言一致;差异行为以 FINDING 记录)。
108+ 
109+核心结论:
110+ 
111+1. Spring Boot 3.x(BOM 管理的 mysql-connector-j 新坐标)配合 openGauss B 模式,基础 CRUD、事务、批量、类型映射、连接池、协议命令均可正常工作;
112+2. **基础功能版本边界不在驱动**:8 档驱动下基础 CRUD/事务/批量/类型映射/连接池/文档参数行为逐项一致,官方文档"8.0.28 及其以下"的版本范围表述可以上探;但**协议命令专项行为与驱动版本相关(三档)**: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 档 37/37 全部通过(详见 4.2);
113+3. 记录 FINDING 7 项(A/B/D/F/G/H/I):FINDING A 实为默认配置(`dolphin.lower_case_table_names=0`)下的大小写敏感行为,设置 `dolphin.lower_case_table_names=1` 并持久化后实测恢复;FINDING B 实为 `dolphin.sql_mode``no_zero_date` 配置行为(默认开启拒绝,去掉后零日期可存储、三档 zeroDateTimeBehavior 读取均生效);FINDING D 为隔离级别映射;FINDING F/G 为协议命令专项新发现,FINDING H/I 为驱动矩阵全量补测新发现(详见 3.3、5.2);原 FINDING C 经补测改写为正向确认(useOldAliasMetadataBehavior 参数在表列别名场景生效,表达式别名场景与文档备注一致)。
114+ 
115+## 3.2 约束说明
116+ 
117+1. 本测试覆盖 JDBC 基础使用场景(连接/类型/批量/事务/连接池/协议命令),未覆盖存储过程与真游标逐批拉取行为(见维度六、七);
118+2. 测试基于 openGauss 7.0.0-RC3,正式 7.0.0-LTS 发布后建议用同一套用例回归;
119+3. 批量性能对比(rewriteBatchedStatements)为单机单次测量,量级参考,非基准测试;
120+4. 本次未实测 caching_sha2_password 认证方式与 TLS/SSL 连接(需服务端 SSL 配置),版本范围结论仅针对 mysql_native_password 认证与 JDBC 驱动,不覆盖 MySQL 命令行客户端。
121+ 
122+## 3.3 关键风险和规避措施
123+ 
124+| 风险 | 说明 | 规避措施 |
125+| ---- | ---- | -------- |
126+| FINDING A:JDBC 元数据 API 不可用 | `getTables` 报错 `information_schema.TABLES does not exist`——默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感:视图本身存在、小写可查,驱动按 MySQL 惯例发大写 TABLES 报不存在,依赖元数据探测的 ORM/工具可能受影响 | **将 `dolphin.lower_case_table_names` 设为 1 并持久化(postgresql.conf 追加后重启)**,实测 `DatabaseMetaData.getTables` 恢复正常;未配置时可使用 B 模式支持的元数据方式(直接查询系统表)或避开该 API |
127+| FINDING B:零日期 | 零日期(`0000-00-00`)存储由 `dolphin.sql_mode``no_zero_date` 控制:默认开启,写入被拒(`date/time field value out of range`);从 sql_mode 中去掉后可正常存储 | 需要存储零日期时去掉 `no_zero_date`;表中有零日期时 `zeroDateTimeBehavior` 三档读取实测均生效(convertToNull 读为 null、exception 抛 `Zero date value prohibited`、round 读为 `0001-01-01`,与 MySQL 语义一致) |
128+| FINDING C(已改写为正向确认) | `useOldAliasMetadataBehavior` 表列别名场景参数生效(true 时 getColumnName 返回别名),表达式别名场景两种设置一致是文档规定的预期行为 | 无风险 |
129+| FINDING D:隔离级别映射 | SERIALIZABLE 被服务端映射为 REPEATABLE_READ(读回 4) | 应用按实际生效级别(REPEATABLE_READ)设计并发控制 |
130+| FINDING F:SEND_LONG_DATA 二进制参数遇 0x00 截断 | 服务端预编译(useServerPrepStmts=true)+ setBlob 流式写入 LONGBLOB 时,二进制内容遇第一个 0x00 字节被服务端按 C 字符串截断(仅存前缀);无零字节的 1MB 数据完整往返(MD5 一致);客户端预编译(文本协议)路径无此问题 | 服务端预编译路径避免写入含 0x00 的二进制数据(使用客户端预编译文本协议路径),或修复后回归 |
131+| FINDING G:多语句结果返回结构不完整 | `allowMultiQueries=true` 多语句执行时,execute() 只反映首语句结果,getMoreResults() 取不到后续语句的结果集(B 组诊断:仅回传首语句结果 + 末 DML update count,中间结果全部丢弃;纯 SELECT 序列则逐个正常返回);各语句的 DML 均已落库 | 逐条执行或仅依赖末条 update count;服务端修复后回归 |
132+| FINDING H:SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档) | 服务端预编译 + 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 档完整往返。断言行固定 `expected: 1048576 but was: 1040384`,触发条件与驱动分块方式相关(8192 字节对齐边界),缺陷在服务端 | 8.0.28~8.4.0 档服务端预编译路径避免写入 8192 倍数大小的大二进制(改用 9.7.0+ 驱动或客户端预编译文本协议路径);服务端修复后回归 |
133+| 存储过程 / 真游标 | 存储过程 prepareCall 报 `PARAMETERS does not exist` 实为默认配置下大小写敏感所致(视图存在、小写可查,`lower_case_table_names=1` 后大写可查);协议层对游标与存储过程功能支持有限(文档已声明) | 改写为普通 SQL / 批处理;详见维度六、七 |
134+| FINDING I:COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档) | 8.0.28 档 clearParameters 后二次 executeUpdate(驱动先发 COM_STMT_RESET)直接崩溃 openGauss 服务端进程:客户端报 `EOFException: read 0 bytes before connection was unexpectedly lost`(响应中途断连),服务端日志在崩溃时刻戛然而止(无 PANIC、无正常关闭流程),3308 端口不可达,需人工重启;连续复现 3 次(单测可复现,两个工程同构)。8.0.33 及以上档同一序列正常,触发条件依赖驱动版本 | 8.0.28 档避免服务端预编译语句 clearParameters 后复用(改为新建语句或换 8.0.33+ 驱动);服务端修复后回归 |
135+ 
136+# 4 版本详细测试结论
137+ 
138+## 4.1 特性测试结论
139+ 
140+### 4.1.1 连接与认证(4 用例,全部通过)
141+ 
142+| 用例 | 结果 |
143+| ---- | ---- |
144+| 正常连接(url 指向 3308 协议口) | ✓;productName = `MySQL`,productVersion = `8.0.28-dophin-server`(服务端返回的官方拼写) |
145+| 错误密码 | ✓ 拒绝:`failed in auth check, role:mysql_test_db` |
146+| 错误用户名 | ✓ 拒绝:`no pg_hba.conf entry ...` |
147+| mysql 驱动连 5432 原生口 | ✓ 失败:`Communications link failure`(MySQL 协议只在 3308) |
148+| FINDING A | `DatabaseMetaData.getTables` 报错 `relation "information_schema.TABLES" does not exist on primary`(默认配置 `dolphin.lower_case_table_names=0` 下大小写敏感:视图存在、小写可查;设置 =1 并持久化后实测 getTables 恢复正常) |
149+ 
150+### 4.1.2 类型映射与文档参数(8 用例,全部通过)
151+ 
152+| 类型 / 参数 | 结果 |
153+| ----------- | ---- |
154+| TINYINT → Integer | ✓ |
155+| TINYINT(1) → Boolean(tinyInt1isBit 默认 true) | ✓ |
156+| BIT(1) → Boolean | ✓ 与官方协议兼容文档备注一致("不管是否转换,在 Java 中都是 Boolean 类型") |
157+| 服务端列类型元数据 | ✓ 观测:TINYINT(1) 与 BIT(1) 均上报 `typeName=BIT`(jdbcType=-7)——默认参数下两者均映射 Boolean 的根源 |
158+| DECIMAL(10,2) → BigDecimal | ✓ |
159+| DATE → java.sql.Date / DATETIME → LocalDateTime / TIMESTAMP → Timestamp | ✓ |
160+| VARCHAR → String / BLOB 读写往返(9 字节) | ✓ |
161+| `tinyInt1isBit=false` → Integer | ✓ 参数生效 |
162+| `transformedBitIsBoolean=false/true` | ✓ 两种设置 getObject 均映射 Boolean(与官方文档备注一致);getBytes 恒返回原始字节 `{1}`(与参数无关)。原 1.0 版"false→byte[]"结论系 getBytes 观测口径,已修正 |
163+| `useOldAliasMetadataBehavior=true/默认` | ✓ **参数生效(原 FINDING C 改写为正向确认)**:表列别名 `SELECT name AS n` 场景,true 时 getColumnName 返回别名 `n`,默认返回原始列名 `name`;表达式别名 `SELECT 1 AS my_alias` 场景两种设置均返回别名,符合文档备注"表达式别名始终返回别名" |
164+| FINDING B | ✓ 零日期由 `dolphin.sql_mode``no_zero_date` 控制:默认开启,`0000-00-00 00:00:00` 写入被拒(`date/time field value out of range`);会话级去掉 no_zero_date 后可正常存储;表中有零日期时 zeroDateTimeBehavior 三档实测均生效——convertToNull 读为 null、exception 读时抛 `Zero date value prohibited`、round 读为 `0001-01-01`(与 MySQL 语义一致) |
165+| `useaffectedrows=true/默认` | ✓ 与官方文档备注一致("目前不支持"):无变化 UPDATE 两种设置均返回 1(找到的行数);MySQL 原生在 =true 时应返回 0(受影响行数),B 模式恒返回找到行数 |
166+| `allowMultiQueries=true/默认` | ✓ 参数生效:true 时 `SELECT 1; SELECT 2` 两个结果集均返回;默认时服务端明确拒绝(`Multi-statement is not allowed. Please connect with allowMultiQueries=true`) |
167+ 
168+### 4.1.3 DML 批量(3 用例,全部通过)
169+ 
170+| 用例 | 结果 |
171+| ---- | ---- |
172+| PreparedStatement.addBatch 1000 行 | ✓ executeBatch 返回 1000 条计数 |
173+| rewriteBatchedStatements=true vs 默认 | ✓ **约 17 倍提速**(500 行 59ms vs 1020ms)→ B 模式协议层真实支持批量 rewrite,建议指导文档推荐开启 |
174+| JdbcTemplate.batchUpdate | ✓ |
175+ 
176+### 4.1.4 事务(5 用例,全部通过)
177+ 
178+| 用例 | 结果 |
179+| ---- | ---- |
180+| TransactionTemplate 提交后可见 / setRollbackOnly 回滚后不可见 | ✓ |
181+| 手动 Connection commit / rollback | ✓ |
182+| SAVEPOINT 保存点 + 部分回滚 | ✓ |
183+| 隔离级别 READ_COMMITTED / REPEATABLE_READ / SERIALIZABLE | ✓ 均被接受 |
184+| FINDING D | setTransactionIsolation(SERIALIZABLE) 后读回 4(REPEATABLE_READ)→ 服务端将 SERIALIZABLE 映射/回落为 REPEATABLE_READ |
185+ 
186+### 4.1.5 连接池(2 用例,全部通过)
187+ 
188+| 用例 | 结果 |
189+| ---- | ---- |
190+| Boot 默认 HikariCP,maximumPoolSize=10 生效 | ✓ |
191+| CONNECTION_ID() 两次取连接 id 相同(池大小=1) | ✓ 连接复用确认;CONNECTION_ID() 为 B 模式支持的 MySQL 函数,可作为文档补充点 |
192+ 
193+### 4.1.6 已知限制专项(3 用例)
194+ 
195+| 用例 | 结果 |
196+| ---- | ---- |
197+| 游标:useCursorFetch=true + useServerPrepStmts=true + setFetchSize(100) | 驱动层无报错、完整读回 2000 行;**协议层是否真逐批返回待确认**(无报错≠真游标,报告按"驱动层可用、协议层待确认"记录) |
198+| 普通 Statement.setFetchSize(50) | 无报错、完整读回(可能被静默忽略) |
199+| 存储过程 CallableStatement | ✓ 报错(符合文档"不支持"):`relation "information_schema.PARAMETERS" does not exist on primary` —— 技术根源:实为默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感所致,PARAMETERS 视图存在、小写 parameters 实测可查,设置 `dolphin.lower_case_table_names=1` 后大写可查;协议层对存储过程功能支持有限 |
200+ 
201+### 4.1.7 冒烟(3 用例,全部通过)
202+ 
203+基于 `@SpringBootTest` 全链路冒烟(JdbcTemplate / TxService),覆盖示例工程主干业务路径:
204+ 
205+| 用例 | 结果 |
206+| ---- | ---- |
207+| JdbcTemplate CRUD(insert → select → update → delete) | ✓ |
208+| JdbcTemplate batchUpdate 批量插入 | ✓ 返回 2 条计数 |
209+| 事务回滚(TxService 抛异常触发回滚) | ✓ 回滚后表中无残留行 |
210+ 
211+### 4.1.8 协议命令专项(9 用例)
212+ 
213+> 背景:评审意见要求增加协议命令基础功能测试(COM_STMT_PREPARE / COM_STMT_SEND_LONG_DATA / COM_STMT_EXECUTE,最好覆盖到每个 command)。MySQL 协议命令按"JDBC 是否有触发路径"处理:可触发的逐条实测(用例 7.1-7.9),无触发路径的在下方说明。
214+ 
215+| 协议命令 | JDBC 触发方式 | 用例 | 结果 |
216+| -------- | ------------- | ---- | ---- |
217+| COM_QUERY | 普通 Statement 执行 SQL(文本协议,建表/插入/查询均走此命令) | 7.1 | ✓ 插入 2 行后计数 +2(文本协议插入/查询/计数正常) |
218+| COM_INIT_DB | url 指定 database 建立连接(握手携带);setCatalog + useLocalSessionState=false | 7.2 | ✓ 连接 catalog=测试库;setCatalog 切换后查询正常 |
219+| COM_PING | Connection.isValid(2) | 7.3 | ✓ isValid=true |
220+| COM_STMT_PREPARE | useServerPrepStmts=true 下 prepareStatement | 7.4 | ✓ 服务端预编译插入+查询往返正常(cachePrepStmts=false 确保真实发出) |
221+| COM_STMT_EXECUTE | 服务端预编译语句 executeQuery/executeUpdate | 7.4 | ✓ 同 7.4 |
222+| COM_STMT_CLOSE | 服务端预编译语句 close | 7.4 | ✓ 同 7.4(语句关闭无异常) |
223+| COM_STMT_SEND_LONG_DATA | 服务端预编译 + setBlob 流式写入 1MB LONGBLOB(MD5 往返校验) | 7.5 | ✓ 链路可用(9.7.0 基线),但发现 FINDING F:无零字节 1MB(全 0x41)完整往返 MD5 一致;含 0x00 的 200 字节读回仅 150 字节(= 第一个 0x00 前的前缀)——服务端对二进制参数按 C 字符串截断(详见 5.2.2,回归断言)。驱动矩阵补测发现 FINDING H:8.0.28~8.4.0 档无零字节 1MB 读回仅 1040384 字节(丢末块 8KB,详见 4.2/5.2.2) |
224+| COM_STMT_RESET | clearParameters 后复用同一预编译语句 | 7.6 | ✓(8.0.33 及以上档)clearParameters 后复用同一语句插入 2 行均生效;但 8.0.28 档此序列(COM_STMT_RESET)可复现崩溃服务端进程(FINDING I,详见 5.2.2) |
225+| COM_STMT_FETCH | useCursorFetch=true + setFetchSize(100) | 7.7 | ✓ 驱动层无报错、完整读回 500 行;协议层是否真逐批返回需服务端日志佐证(口径同 6.1,按"驱动层可用、协议层待确认"记录) |
226+| COM_SET_OPTION | allowMultiQueries=true 多语句执行 | 7.8 | ✓ 参数生效、多语句 DML 全部落库,但发现 FINDING G:execute() 仅返回首语句 update count,getMoreResults() 取不到后续 SELECT 结果集(后续 SELECT 被丢弃;纯 SELECT 序列逐个正常)(详见 5.2.2,回归断言) |
227+| COM_QUIT | Connection.close | 7.9 | ✓ close 后 isClosed=true |
228+ 
229+JDBC 无触发路径的命令:COM_FIELD_LIST(已被查询结果集元数据取代)、COM_PROCESS_INFO / COM_STATISTICS / COM_PROCESS_KILL(管理类命令)、COM_CHANGE_USER、COM_REFRESH 等,MySQL JDBC 驱动不提供对应 API,无法从应用层触发,不纳入用例;本专项已覆盖**驱动可触发的全部协议命令**
230+ 
231+## 4.2 兼容性测试结论(驱动版本矩阵)
232+ 
233+同一套 37 用例扫描 8 档驱动(8 个 Maven profile 切换,同一服务端环境;其中 BOM 默认档与显式 9.7.0 为同一版本的两条管理路径):
234+ 
235+| 驱动版本 | 坐标 | 结果 |
236+| -------- | ---- | ---- |
237+| 8.0.28(文档基准) | mysql:mysql-connector-java(旧坐标) | 协议命令类 1 失败 + 6 错误:SEND_LONG_DATA 丢末块 8KB(FINDING H)、COM_STMT_RESET 崩溃服务端进程(FINDING I,后续用例全部失联,需人工重启服务端);其余 28 用例全部通过 |
238+| 8.0.33 | com.mysql:mysql-connector-j(新坐标) | 37 用例中 1 失败(FINDING H:SEND_LONG_DATA 丢末块 8KB),其余 36 通过(COM_STMT_RESET 正常、服务端未崩溃) |
239+| 8.1.0 | 同上 | 同上(1 失败 + 36 通过) |
240+| 8.3.0 | 同上 | 同上(1 失败 + 36 通过) |
241+| 8.4.0 | 同上 | 同上(1 失败 + 36 通过) |
242+| 9.7.0(Boot 3.5.16 BOM 默认档) | com.mysql:mysql-connector-j(版本由 BOM 管理) | 37/37 通过 |
243+| 9.7.0(显式 pin) | 同上 | 与 BOM 默认档同一版本同一 jar(仅 Maven 管理路径不同),行为同 BOM 默认档实测 37/37 |
244+| 26.7.0(最新) | 同上 | 37/37 通过 |
245+ 
246+8 档驱动的类型映射、批量提速、事务隔离映射、元数据报错(大小写敏感)、零日期行为(默认拒绝/去掉 no_zero_date 可存储/三档读取生效)、游标行为、存储过程报错、6 个文档参数行为、多语句结果结构(FINDING G)**逐项一致**;协议命令行为按驱动版本分**三档**:8.0.28 崩溃 / 8.0.33~8.4.0 丢 8KB / 9.7.0 及以上正常(FINDING H/I,详见 5.2.2)。
247+ 
248+**结论:基础功能(CRUD/事务/批量/类型映射/连接池/文档参数)与驱动版本无关,8 档均可正常工作;协议命令专项存在两个与驱动版本相关的服务端缺陷(FINDING H/I)。** 建议官方文档的版本范围表述由"MySQL JDBC driver:8.0.28 及其以下"更新为"MySQL JDBC 驱动 8.0.28 及以上版本在基础 CRUD/事务/批量/类型映射场景均可正常工作;服务端预编译 + SEND_LONG_DATA/RESET 场景建议使用 9.7.0 及以上驱动"(对应 docs 仓 `dolphin_mysql_protocol_compatibility` 文档 PR,仅针对 JDBC 驱动表述;MySQL 命令行客户端未实测)。
249+ 
250+# 5 测试对象质量评估
251+ 
252+## 5.1 覆盖率分析
253+ 
254+| 覆盖维度 | 覆盖情况 |
255+| -------- | -------- |
256+| 功能覆盖 | 连接/认证/类型映射/DML 批量/事务/连接池/已知限制/协议命令七维度 34 用例 + 冒烟 3 用例;官方协议兼容文档列出的 6 个 JDBC 参数与 JDBC 可触发的全部协议命令均已覆盖 |
257+| 版本覆盖 | 驱动 8 档(BOM 默认档 + 7 个显式版本,8.0.28 ~ 26.7.0,覆盖新旧两套 Maven 坐标) |
258+| 未覆盖项 | 存储过程(协议不支持,仅验证报错行为)、真游标协议层逐批返回行为(7.7 已覆盖驱动层路径,服务端行为待抓包确认)、caching_sha2_password 认证、TLS/SSL 连接、MySQL 命令行客户端、高并发场景、故障注入、JDBC 无触发路径的协议命令(COM_FIELD_LIST/COM_PROCESS_INFO/COM_STATISTICS/COM_PROCESS_KILL/COM_CHANGE_USER 等,见 4.1.8) |
259+ 
260+## 5.2 缺陷统计和分析
261+ 
262+### 5.2.1 缺陷统计
263+ 
264+| 类别 | 数量 | 说明 |
265+| ---- | ---- | ---- |
266+| 服务端差异(FINDING) | 5 | A/B/D/F/G:A/D 为行为差异或元数据缺失,B 为配置行为(no_zero_date 可去),F/G 为协议命令专项新发现(二进制参数 0x00 截断 / 多语句结果结构不完整);均非崩溃类缺陷(原 FINDING C 经补测改写为正向确认,见 4.1.2) |
267+| 崩溃类缺陷 | 0 | 无 |
268+ 
269+### 5.2.2 缺陷列表
270+ 
271+| 编号 | 现象 | 定位 | 严重程度 | 规避措施 |
272+| ---- | ---- | ---- | -------- | -------- |
273+| FINDING A | getTables 报错 information_schema.TABLES 不存在 | 默认配置(`dolphin.lower_case_table_names=0`)下大小写敏感:视图存在、小写 tables 实测可查,驱动按 MySQL 惯例发大写 TABLES 报不存在 | 中(影响依赖元数据探测的工具/ORM) | **`dolphin.lower_case_table_names=1` 并持久化(实测 getTables 恢复正常)**;未配置时避开该 API |
274+| FINDING B | 零日期写入被拒 | `dolphin.sql_mode``no_zero_date` 默认开启(写入报 `date/time field value out of range`);去掉后可存储,此时 zeroDateTimeBehavior 三档读取均生效 | 低(默认拒绝符合数据库常规语义,且可按需放开) | 需要存储零日期时去掉 no_zero_date;否则应用层避免零日期 |
275+| FINDING D | SERIALIZABLE 读回 REPEATABLE_READ | 服务端隔离级别映射 | 中(并发控制设计需注意) | 按 REPEATABLE_READ 设计 |
276+| FINDING F | COM_STMT_SEND_LONG_DATA 写入含 0x00 的二进制被截断 | 服务端预编译路径对二进制参数按 C 字符串处理:遇第一个 0x00 截断为前缀(200 字节读回 150 = 0x00 位置);无零字节 1MB 完整往返(MD5 一致);客户端预编译(文本协议)路径无此问题 | 中(影响服务端预编译 + 二进制大对象写入的正确性) | 服务端预编译路径避免写入含 0x00 的二进制(用客户端预编译文本协议路径) |
277+| FINDING G | 多语句后续 SELECT 结果集经 getMoreResults() 不可得 | `allowMultiQueries=true` 下结果返回结构不完整:仅回传首语句结果 + 末 DML update count,中间结果全部丢弃(纯 SELECT 序列逐个正常);各语句 DML 均已落库 | 中(影响依赖多结果集解析的应用/框架) | 逐条执行或仅依赖末条 update count |
278+| FINDING H | SEND_LONG_DATA 1MB 丢末块 8KB(8.0.28~8.4.0 档) | 服务端预编译 + 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+ 驱动或客户端预编译文本协议路径) |
279+| FINDING I | COM_STMT_RESET 崩溃服务端进程(仅 8.0.28 档) | 8.0.28 档 clearParameters 后二次 executeUpdate(驱动先发 COM_STMT_RESET)直接崩溃 openGauss 服务端:客户端报 `EOFException: read 0 bytes before connection was unexpectedly lost`,服务端日志戛然而止(无 PANIC、无正常关闭流程),3308 不可达需人工重启;单测可复现 3 次(两个工程同构);8.0.33+ 档同一序列正常 | 严重(单次 JDBC 操作即可打崩整个服务端,可被任意客户端触发拒绝服务) | 8.0.28 档避免服务端预编译语句 clearParameters 后复用(新建语句或换 8.0.33+ 驱动);服务端修复后回归 |
280+| FINDING(存储过程) | prepareCall 报错 PARAMETERS 不存在 | 默认配置下大小写敏感(小写 parameters 实测存在,`lower_case_table_names=1` 后大写可查;存储过程本身文档已声明不支持) | 低(文档已声明) | 改写为普通 SQL |
281+ 
282+# 6 测试过程评估
283+ 
284+## 6.1 测试策略回顾
285+ 
286+采用"控制变量 + 矩阵扫描"策略:先以 Boot BOM 默认驱动(9.7.0)跑全七维度建立基线,再以 Maven profile 切换驱动版本扫描矩阵;对每一项与 MySQL 官方文档行为的差异,用最小复现定位差异归属(驱动侧 or 服务端侧)。
287+ 
288+## 6.2 测试设计评估
289+ 
290+1. 逐项覆盖了官方协议兼容文档(dolphin_mysql_protocol_compatibility)列出的全部 6 个 JDBC 参数(zeroDateTimeBehavior / tinyInt1isBit / transformedBitIsBoolean / useOldAliasMetadataBehavior / useaffectedrows / allowMultiQueries):3 项验证生效(tinyInt1isBit、useOldAliasMetadataBehavior、allowMultiQueries),3 项行为与文档声明一致(transformedBitIsBoolean:两种设置 Java 侧均映射 Boolean;useaffectedrows:恒返回找到的行数;zeroDateTimeBehavior:去掉 `no_zero_date` 存储零日期后三档读取均生效);
291+2. 覆盖了文档明示的已知限制(游标、存储过程),并定位了存储过程报错的技术根源(默认配置下 information_schema 表名大小写敏感,`dolphin.lower_case_table_names=1` 持久化后大写可查);
292+3. 批量用例同时验证了功能与性能(rewriteBatchedStatements 17 倍提速),为文档推荐参数提供了数据支撑;
293+4. 协议命令专项把评审要求的"覆盖到每个 command"落成 9 个用例:按 JDBC 触发路径逐条实测 COM_QUERY/INIT_DB/PING/STMT_PREPARE/STMT_EXECUTE/STMT_CLOSE/STMT_SEND_LONG_DATA/STMT_RESET/STMT_FETCH/SET_OPTION/QUIT,无 JDBC 触发路径的命令在 4.1.8 说明原因。
294+ 
295+## 6.3 测试执行评估
296+ 
297+### 6.3.1 测试执行统计数据
298+ 
299+| 项目 | 数据 |
300+| ---- | ---- |
301+| 用例总数 | 37 |
302+| 驱动档位 | 8 档(BOM 默认档 + 7 个显式版本;含文档基准 8.0.28 与最新 26.7.0) |
303+| 执行轮次 | 8 档各一轮全量回归(8.0.28 档因 FINDING I 服务端崩溃分两段执行:协议类 9 + 其余 28)+ 若干轮开发调试与单用例复测 |
304+| 总执行用例次 | 37 × 8 = 296(含 8.0.28 档两段) |
305+ 
306+### 6.3.2 测试用例执行结果统计数据
307+ 
308+| 结果 | 数量 |
309+| ---- | ---- |
310+| 通过 | 285(8.0.28 档 30 / 8.0.33~8.4.0 档各 36 / 9.7.0 两档 37+37 / 26.7.0 档 37) |
311+| 失败 | 5(FINDING H 断言失败:8.0.28 档 1 + 8.0.33~8.4.0 档各 1) |
312+| 错误 | 6(FINDING I 服务端崩溃后失联:8.0.28 档 6) |
313+ 
314+# 7 附件
315+ 
316+## 7.1 附件1:相关产物
317+ 
318+| 产物 | 位置 |
319+| ---- | ---- |
320+| 测试代码(37 用例:七维度 34 + 冒烟 3) | 本目录 `src/test/java/org/opengauss/`(cases/ 与 smoke/) |
321+| 测试思维导图 | 本目录 `docs/测试思维导图-SpringBoot连接openGaussB库.{mm,md,html}` |
322+| 指导文档 | docs 仓 `docs/zh/mysql_compatibility_developer_guide/springboot_development.md`(轻量版 `docs-lite/zh/...` 同路径) |
@@ -0,0 +1,139 @@
1+<!DOCTYPE html>
2+<html lang="zh-CN">
3+<head>
4+<meta charset="UTF-8">
5+<title>测试思维导图:Spring Boot + 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-auth > .label { background: #2980b9; }
31+ .b-type > .label { background: #27ae60; }
32+ .b-batch > .label { background: #e67e22; }
33+ .b-tx > .label { background: #8e44ad; }
34+ .b-pool > .label { background: #16a085; }
35+ .b-limit > .label { background: #c0392b; }
36+ .b-proto > .label { background: #d35400; }
37+ .b-matrix> .label { background: #2c3e50; }
38+ .b-concl > .label { background: #1e8449; }
39+ .pass { color: #27ae60; font-weight: bold; }
40+ .foot { text-align: center; color: #95a5a6; font-size: 12px; margin-top: 24px; }
41+</style>
42+</head>
43+<body>
44+<h1>测试思维导图:Spring Boot + MySQL JDBC 连接 openGauss B 库(Issue #27)</h1>
45+<p class="sub">37 个测试用例(BOM 默认 9.7.0 基线全通过)· 8 档驱动矩阵协议命令分三档 · FINDING A/B/D/F/G/H/I 已记录,6 个文档参数逐一实测</p>
46+<ul class="map-tree">
47+ <li class="root"><span class="label">Spring Boot + MySQL JDBC × openGauss B 库 测试</span>
48+ <ul>
49+ <li class="branch b-env"><span class="label">测试环境</span>
50+ <ul>
51+ <li class="leaf"><span class="label">openGauss 7.0.0-RC3(fe6f6e4b)B 库 proto_test_db / schema mysql_test_db</span></li>
52+ <li class="leaf"><span class="label">MySQL 协议端口 3308(enable_dolphin_proto = on)</span></li>
53+ <li class="leaf"><span class="label">Spring Boot 3.5.16 + JDK 17 + mysql-connector-j(BOM 默认 9.7.0)</span></li>
54+ </ul>
55+ </li>
56+ <li class="branch b-auth"><span class="label">一、连接与认证(4 用例 <span class="pass"></span></span>
57+ <ul>
58+ <li class="leaf"><span class="label">正常连接;productName = MySQL,productVersion = 8.0.28-dophin-server</span></li>
59+ <li class="leaf"><span class="label">错误密码 → 拒绝(failed in auth check)</span></li>
60+ <li class="leaf"><span class="label">错误用户名 → 拒绝(no pg_hba.conf entry)</span></li>
61+ <li class="leaf"><span class="label">连 5432 原生口 → 拒绝(Communications link failure)</span></li>
62+ <li class="leaf-note"><span class="label">FINDING A:getTables 报错 information_schema.TABLES 不存在(默认配置 dolphin.lower_case_table_names=0 下大小写敏感:小写可查;=1 并持久化后实测恢复)</span></li>
63+ </ul>
64+ </li>
65+ <li class="branch b-type"><span class="label">二、类型映射 + 6 个文档参数(8 用例 <span class="pass"></span></span>
66+ <ul>
67+ <li class="leaf"><span class="label">9 种类型往返:TINYINT / TINYINT(1)→Boolean / BIT(1)→Boolean / DECIMAL / DATE / DATETIME / TIMESTAMP / VARCHAR / BLOB(服务端将 TINYINT(1) 与 BIT(1) 均上报为 typeName=BIT)</span></li>
68+ <li class="leaf"><span class="label">tinyInt1isBit=false → Integer(参数生效)</span></li>
69+ <li class="leaf"><span class="label">transformedBitIsBoolean=false/true → getObject 均映射 Boolean(与官方文档备注一致)</span></li>
70+ <li class="leaf"><span class="label">useOldAliasMetadataBehavior ✓ 参数生效(原 FINDING C 改写为正向确认):表列别名 true→getColumnName 返回别名 n、默认→原始列名 name;表达式别名两种设置均返回别名(符合文档备注)</span></li>
71+ <li class="leaf"><span class="label">useaffectedrows=true/默认:无变化 UPDATE 均返回 1(与文档备注「始终返回找到行数」一致)</span></li>
72+ <li class="leaf"><span class="label">allowMultiQueries=true → 两条语句两个结果集均返回(参数生效);默认 → 服务端明确拒绝</span></li>
73+ <li class="leaf-note"><span class="label">FINDING B:零日期由 dolphin.sql_mode 的 no_zero_date 控制——默认写入被拒;去掉后可存储;存储后 zeroDateTimeBehavior 三档读取均生效(convertToNull→null / exception→抛错 / round→0001-01-01)</span></li>
74+ </ul>
75+ </li>
76+ <li class="branch b-batch"><span class="label">三、DML 批量(3 用例 <span class="pass"></span></span>
77+ <ul>
78+ <li class="leaf"><span class="label">PreparedStatement.addBatch 1000 行</span></li>
79+ <li class="leaf"><span class="label">JdbcTemplate.batchUpdate</span></li>
80+ <li class="leaf"><span class="label">rewriteBatchedStatements=true 提速约 17 倍(59ms vs 1020ms)</span></li>
81+ </ul>
82+ </li>
83+ <li class="branch b-tx"><span class="label">四、事务(5 用例 <span class="pass"></span></span>
84+ <ul>
85+ <li class="leaf"><span class="label">TransactionTemplate 提交可见 / 回滚不可见</span></li>
86+ <li class="leaf"><span class="label">手动 Connection commit / rollback</span></li>
87+ <li class="leaf"><span class="label">SAVEPOINT 保存点 + 部分回滚</span></li>
88+ <li class="leaf-note"><span class="label">FINDING D:SERIALIZABLE 读回 4(REPEATABLE_READ)</span></li>
89+ </ul>
90+ </li>
91+ <li class="branch b-pool"><span class="label">五、连接池(2 用例 <span class="pass"></span></span>
92+ <ul>
93+ <li class="leaf"><span class="label">HikariCP maximumPoolSize=10 配置生效</span></li>
94+ <li class="leaf"><span class="label">CONNECTION_ID() 两次取连接同 id → 池复用确认</span></li>
95+ </ul>
96+ </li>
97+ <li class="branch b-limit"><span class="label">六、已知限制专项(3 用例)</span>
98+ <ul>
99+ <li class="leaf"><span class="label">游标:驱动层无报错、2000 行完整读回;协议层是否真游标待确认</span></li>
100+ <li class="leaf"><span class="label">Statement.setFetchSize(50) 无报错(可能被静默忽略)</span></li>
101+ <li class="leaf-note"><span class="label">存储过程 CallableStatement 报错 information_schema.PARAMETERS 不存在(默认配置大小写敏感所致;lower_case_table_names=1 后大写可查)</span></li>
102+ </ul>
103+ </li>
104+ <li class="branch b-proto"><span class="label">七、协议命令专项(9 用例 <span class="pass"></span>,评审意见落地)</span>
105+ <ul>
106+ <li class="leaf"><span class="label">7.1 COM_QUERY / 7.2 COM_INIT_DB / 7.3 COM_PING</span></li>
107+ <li class="leaf"><span class="label">7.4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE(useServerPrepStmts=true 全生命周期)</span></li>
108+ <li class="leaf"><span class="label">7.5 COM_STMT_SEND_LONG_DATA:无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)</span></li>
109+ <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>
110+ <li class="leaf-note"><span class="label">FINDING F:含 0x00 二进制被按 C 字符串截断为前缀(200 字节读回 150;客户端预编译路径无此问题)</span></li>
111+ <li class="leaf"><span class="label">7.6 COM_STMT_RESET(clearParameters 后复用,8.0.33 及以上档正常)</span></li>
112+ <li class="leaf-note"><span class="label">FINDING I:8.0.28 档此序列直接崩溃 openGauss 服务端进程(EOFException 断连、可复现 3 次)</span></li>
113+ <li class="leaf"><span class="label">7.7 COM_STMT_FETCH:驱动层可用、协议层待确认</span></li>
114+ <li class="leaf"><span class="label">7.8 COM_SET_OPTION(allowMultiQueries 多语句)</span></li>
115+ <li class="leaf-note"><span class="label">FINDING G:多语句后续 SELECT 结果集 getMoreResults() 不可得(DML 均落库)</span></li>
116+ <li class="leaf"><span class="label">7.9 COM_QUIT;无 JDBC 触发路径的命令在报告 4.1.8 说明</span></li>
117+ </ul>
118+ </li>
119+ <li class="branch b-matrix"><span class="label">驱动版本矩阵(8 档 × 37 用例)</span>
120+ <ul>
121+ <li class="leaf-note"><span class="label">8.0.28(文档基准):FINDING I 崩溃 + FINDING H 丢 8KB,其余 30 通过</span></li>
122+ <li class="leaf"><span class="label">8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:36/37(仅 FINDING H,RESET 正常、服务端不崩)</span></li>
123+ <li class="leaf"><span class="label">9.7.0(Boot 默认)/ 26.7.0(最新):37/37 全部通过</span></li>
124+ <li class="leaf"><span class="label">三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 → <b>基础功能与驱动版本无关,协议命令专项与驱动版本相关(缺陷在服务端)</b></span></li>
125+ </ul>
126+ </li>
127+ <li class="branch b-concl"><span class="label">结论</span>
128+ <ul>
129+ <li class="leaf"><span class="label">Boot 3.x + 新驱动基础 CRUD / 事务 / 批量 / 类型映射 / 连接池可用</span></li>
130+ <li class="leaf"><span class="label">服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动(FINDING H/I)</span></li>
131+ <li class="leaf"><span class="label">支撑 docs 仓版本范围表述更新:"8.0.28 及以上均可正常工作"</span></li>
132+ </ul>
133+ </li>
134+ </ul>
135+ </li>
136+</ul>
137+<p class="foot">测试代码与自测报告见同目录 · 2026-09-04(v1.4)</p>
138+</body>
139+</html>
@@ -0,0 +1,108 @@
1+# 测试思维导图:Spring Boot + MySQL JDBC 连接 openGauss B 库(Issue #27)
2+ 
3+> 大纲版思维导图。可视化版见同名 .html,可编辑版见同名 .mm(XMind / FreeMind / MindMaster 均可打开)。
4+ 
5+```text
6+Spring Boot + MySQL JDBC 连接 openGauss B 库 测试(37 用例;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 + JDK 17 + mysql-connector-j(BOM 默认 9.7.0)
12+
13+├── 一、连接与认证(4 用例 ✓)
14+│ ├── 正常连接;productName = MySQL,productVersion = 8.0.28-dophin-server
15+│ ├── 错误密码 → 拒绝(failed in auth check)✓
16+│ ├── 错误用户名 → 拒绝(no pg_hba.conf entry)✓
17+│ ├── 连 5432 原生口 → 拒绝(Communications link failure)✓
18+│ └── FINDING A:DatabaseMetaData.getTables 报错
19+│ information_schema.TABLES 不存在(默认配置 dolphin.lower_case_table_names=0
20+│ 下大小写敏感:小写可查;设置 =1 并持久化后实测 getTables 恢复正常)
21+
22+├── 二、类型映射 + 6 个文档参数(8 用例 ✓)
23+│ ├── 9 种类型往返:TINYINT / TINYINT(1)→Boolean / BIT(1)→Boolean /
24+│ │ DECIMAL→BigDecimal / DATE / DATETIME / TIMESTAMP / VARCHAR / BLOB
25+│ │ (服务端将 TINYINT(1) 与 BIT(1) 均上报为 typeName=BIT)
26+│ ├── tinyInt1isBit=false → 返回 Integer ✓(参数生效)
27+│ ├── transformedBitIsBoolean=false/true → getObject 均映射 Boolean ✓
28+│ │ (与官方文档备注"Java 侧恒为 Boolean"一致)
29+│ ├── FINDING B:零日期由 dolphin.sql_mode 的 no_zero_date 控制
30+│ │ ├── 默认开启 → "0000-00-00" 写入被拒(date/time field value out of range)
31+│ │ ├── 去掉 no_zero_date → 可正常存储
32+│ │ └── 存储后 zeroDateTimeBehavior 三档读取均生效(与 MySQL 语义一致):
33+│ │ convertToNull→null / exception→抛 Zero date value prohibited / round→0001-01-01
34+│ ├── 原 FINDING C 改写为正向确认:useOldAliasMetadataBehavior ✓
35+│ │ 表列别名(SELECT name AS n):true 时 getColumnName 返回别名 n,
36+│ │ 默认返回原始列名 name(参数生效);
37+│ │ 表达式别名(SELECT 1 AS my_alias):两种设置均返回别名(符合文档备注)
38+│ ├── useaffectedrows=true/默认:无变化 UPDATE 均返回 1(找到行数)✓
39+│ │ (与官方文档备注"目前不支持,始终返回找到行数"一致;
40+│ │ MySQL 原生 =true 时应返回 0)
41+│ └── allowMultiQueries=true → 两条语句两个结果集均返回 ✓(参数生效);
42+│ 默认 → 服务端明确拒绝(Multi-statement is not allowed)
43+
44+├── 三、DML 批量(3 用例 ✓)
45+│ ├── PreparedStatement.addBatch 1000 行 ✓
46+│ ├── JdbcTemplate.batchUpdate ✓
47+│ └── rewriteBatchedStatements=true 提速约 17 倍
48+│ (500 行 59ms vs 1020ms)→ 建议指导文档推荐开启
49+
50+├── 四、事务(5 用例 ✓)
51+│ ├── TransactionTemplate 提交后可见 / setRollbackOnly 回滚后不可见 ✓
52+│ ├── 手动 Connection commit / rollback ✓
53+│ ├── SAVEPOINT 保存点 + 部分回滚 ✓
54+│ └── FINDING D:setTransactionIsolation(SERIALIZABLE)
55+│ 读回 4 = REPEATABLE_READ(服务端映射/回落)
56+
57+├── 五、连接池(2 用例 ✓)
58+│ ├── Boot 默认 HikariCP,maximumPoolSize=10 配置生效 ✓
59+│ └── CONNECTION_ID() 两次取连接返回同一 id → 池复用确认 ✓
60+
61+├── 六、已知限制专项(3 用例)
62+│ ├── 游标:useCursorFetch=true + useServerPrepStmts=true + setFetchSize(100)
63+│ │ 驱动层无报错、完整读回 2000 行;协议层是否真游标待确认(如实记录)
64+│ ├── Statement.setFetchSize(50) 无报错(可能被静默忽略)
65+│ └── 存储过程 CallableStatement 报错
66+│ information_schema.PARAMETERS 不存在(默认配置大小写敏感所致;
67+│ lower_case_table_names=1 后大写可查;存储过程本身文档不支持)
68+
69+├── 七、协议命令专项(9 用例 ✓,评审意见落地)
70+│ ├── 7.1 COM_QUERY / 7.2 COM_INIT_DB / 7.3 COM_PING ✓
71+│ ├── 7.4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE
72+│ │ (useServerPrepStmts=true 全生命周期)✓
73+│ ├── 7.5 COM_STMT_SEND_LONG_DATA(服务端预编译 + 1MB LONGBLOB)
74+│ │ ├── 无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)✓
75+│ │ ├── FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,
76+│ │ │ 丢末块 8KB,MD5 失败;9.7.0/26.7.0 档完整)
77+│ │ └── FINDING F:含 0x00 二进制被按 C 字符串截断为前缀
78+│ │ (200 字节读回 150 = 首个 0x00 位置;客户端预编译路径无此问题)
79+│ ├── 7.6 COM_STMT_RESET(clearParameters 后复用)✓(8.0.33 及以上档)
80+│ │ └── FINDING I:8.0.28 档此序列直接崩溃 openGauss 服务端进程
81+│ │ (EOFException 断连、3308 不可达、可复现 3 次)
82+│ ├── 7.7 COM_STMT_FETCH(useCursorFetch + setFetchSize)✓
83+│ │ 驱动层可用、协议层待确认
84+│ ├── 7.8 COM_SET_OPTION(allowMultiQueries 多语句)
85+│ │ └── FINDING G:后续 SELECT 结果集 getMoreResults() 不可得
86+│ │ (仅首语句结果 + 末 DML update count;DML 均落库)
87+│ └── 7.9 COM_QUIT ✓;无 JDBC 触发路径的命令在报告 4.1.8 说明
88+
89+├── 驱动版本矩阵(8 档 × 37 用例)
90+│ ├── 8.0.28(文档基准,旧坐标 mysql-connector-java)
91+│ │ └── FINDING I:COM_STMT_RESET 崩溃服务端;FINDING H:丢 8KB;
92+│ │ 其余 30 用例通过(协议类 2 通过 + 1 失败 + 6 错误)
93+│ ├── 8.0.33 / 8.1.0 / 8.3.0 / 8.4.0(新坐标 mysql-connector-j)
94+│ │ └── 各档 36/37:仅 FINDING H(SEND_LONG_DATA 丢末块 8KB)失败,
95+│ │ RESET 正常、服务端不崩
96+│ ├── 9.7.0(Boot 3.5.16 BOM 默认)/ 26.7.0(最新)
97+│ │ └── 37/37 全部通过(FINDING H/I 均不触发)
98+│ └── 三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 →
99+│ 基础功能与驱动版本无关,协议命令专项与驱动版本相关(缺陷在服务端)
100+
101+└── 结论
102+ ├── Spring Boot 3.x + 新驱动(≥8.0.31 新坐标)基础 CRUD / 事务 /
103+ │ 批量 / 类型映射 / 连接池均可正常工作
104+ ├── 服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动
105+ │ (FINDING H/I,详见报告 4.2/5.2.2)
106+ └── 支撑 docs 仓版本范围表述更新:
107+ "MySQL JDBC 驱动 8.0.28 及以上版本均可正常工作"
108+```
@@ -0,0 +1,69 @@
1+<map version="1.0.1">
2+<node TEXT="Spring Boot + MySQL JDBC 连接 openGauss B 库 测试(37 用例;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 + JDK 17 + mysql-connector-j(BOM 默认 9.7.0)" POSITION="right"/>
7+</node>
8+<node TEXT="一、连接与认证(4 用例,全部通过)" POSITION="right">
9+<node TEXT="正常连接:productName=MySQL,productVersion=8.0.28-dophin-server" POSITION="right"/>
10+<node TEXT="错误密码拒绝(failed in auth check)" POSITION="right"/>
11+<node TEXT="错误用户名拒绝(no pg_hba.conf entry)" POSITION="right"/>
12+<node TEXT="连 5432 原生口拒绝(Communications link failure)" POSITION="right"/>
13+<node TEXT="FINDING A:getTables 报错 information_schema.TABLES 不存在(默认配置 lower_case_table_names=0 下大小写敏感;=1 持久化后实测恢复)" POSITION="right"/>
14+</node>
15+<node TEXT="二、类型映射 + 6 个文档参数(8 用例,全部通过)" POSITION="right">
16+<node TEXT="9 种类型往返:TINYINT / TINYINT(1)→Boolean / BIT(1)→Boolean / DECIMAL→BigDecimal / DATE / DATETIME / TIMESTAMP / VARCHAR / BLOB(服务端将 TINYINT(1) 与 BIT(1) 均上报为 typeName=BIT)" POSITION="right"/>
17+<node TEXT="tinyInt1isBit=false → Integer(参数生效)" POSITION="right"/>
18+<node TEXT="transformedBitIsBoolean=false/true → getObject 均映射 Boolean(与官方文档备注一致)" POSITION="right"/>
19+<node TEXT="FINDING B:零日期由 dolphin.sql_mode 的 no_zero_date 控制(默认拒绝 → 去掉可存储 → zeroDateTimeBehavior 三档读取均生效:convertToNull→null / exception→抛错 / round→0001-01-01)" POSITION="right"/>
20+<node TEXT="原 FINDING C 改写为正向确认:useOldAliasMetadataBehavior 参数生效(表列别名 true→返回别名 n,默认→原始列名 name;表达式别名两种设置均返回别名,符合文档备注)" POSITION="right"/>
21+<node TEXT="useaffectedrows=true/默认:无变化 UPDATE 均返回 1(与文档备注「目前不支持,始终返回找到行数」一致)" POSITION="right"/>
22+<node TEXT="allowMultiQueries=true:两条语句两个结果集均返回(参数生效);默认:服务端明确拒绝" POSITION="right"/>
23+</node>
24+<node TEXT="三、DML 批量(3 用例,全部通过)" POSITION="right">
25+<node TEXT="PreparedStatement.addBatch 1000 行" POSITION="right"/>
26+<node TEXT="JdbcTemplate.batchUpdate" POSITION="right"/>
27+<node TEXT="rewriteBatchedStatements=true 提速约 17 倍(59ms vs 1020ms)" POSITION="right"/>
28+</node>
29+<node TEXT="四、事务(5 用例,全部通过)" POSITION="right">
30+<node TEXT="TransactionTemplate 提交可见 / 回滚不可见" POSITION="right"/>
31+<node TEXT="手动 Connection commit / rollback" POSITION="right"/>
32+<node TEXT="SAVEPOINT 保存点 + 部分回滚" POSITION="right"/>
33+<node TEXT="FINDING D:SERIALIZABLE 读回 4(REPEATABLE_READ)" POSITION="right"/>
34+</node>
35+<node TEXT="五、连接池(2 用例,全部通过)" POSITION="right">
36+<node TEXT="HikariCP maximumPoolSize=10 配置生效" POSITION="right"/>
37+<node TEXT="CONNECTION_ID() 两次取连接同 id → 池复用确认" POSITION="right"/>
38+</node>
39+<node TEXT="六、已知限制专项(3 用例)" POSITION="right">
40+<node TEXT="游标:驱动层无报错、2000 行完整读回;协议层是否真游标待确认" POSITION="right"/>
41+<node TEXT="Statement.setFetchSize(50) 无报错(可能被静默忽略)" POSITION="right"/>
42+<node TEXT="存储过程 CallableStatement 报错 information_schema.PARAMETERS 不存在(默认配置大小写敏感所致;lower_case_table_names=1 后大写可查)" POSITION="right"/>
43+</node>
44+<node TEXT="七、协议命令专项(9 用例,评审意见落地)" POSITION="right">
45+<node TEXT="7.1 COM_QUERY / 7.2 COM_INIT_DB / 7.3 COM_PING" POSITION="right"/>
46+<node TEXT="7.4 COM_STMT_PREPARE / EXECUTE / CLOSE(useServerPrepStmts=true 全生命周期)" POSITION="right"/>
47+<node TEXT="7.5 COM_STMT_SEND_LONG_DATA:无零字节 1MB 完整往返(MD5 一致,9.7.0 基线)" POSITION="right"/>
48+<node TEXT="FINDING H:8.0.28~8.4.0 档读回仅 1040384 字节(= 127×8192,丢末块 8KB);9.7.0/26.7.0 档完整" POSITION="right"/>
49+<node TEXT="FINDING F:含 0x00 二进制被按 C 字符串截断为前缀(200 字节读回 150;客户端预编译路径无此问题)" POSITION="right"/>
50+<node TEXT="7.6 COM_STMT_RESET(clearParameters 后复用,8.0.33 及以上档正常)" POSITION="right"/>
51+<node TEXT="FINDING I:8.0.28 档 COM_STMT_RESET 直接崩溃 openGauss 服务端进程(EOFException 断连、3308 不可达、可复现 3 次)" POSITION="right"/>
52+<node TEXT="7.7 COM_STMT_FETCH:驱动层可用、协议层待确认" POSITION="right"/>
53+<node TEXT="7.8 COM_SET_OPTION(allowMultiQueries)" POSITION="right"/>
54+<node TEXT="FINDING G:多语句后续 SELECT 结果集 getMoreResults() 不可得(DML 均落库)" POSITION="right"/>
55+<node TEXT="7.9 COM_QUIT;无 JDBC 触发路径的命令在报告 4.1.8 说明" POSITION="right"/>
56+</node>
57+<node TEXT="驱动版本矩阵(8 档 × 37 用例)" POSITION="right">
58+<node TEXT="8.0.28(文档基准,旧坐标):FINDING I 崩溃 + FINDING H 丢 8KB,其余 30 通过" POSITION="right"/>
59+<node TEXT="8.0.33 / 8.1.0 / 8.3.0 / 8.4.0:36/37,仅 FINDING H(RESET 正常、服务端不崩)" POSITION="right"/>
60+<node TEXT="9.7.0(Boot 默认)/ 26.7.0(最新):37/37 全部通过" POSITION="right"/>
61+<node TEXT="三档行为:8.0.28 崩 / 8.0.33~8.4.0 丢数据 / 9.7.0+ 正常 → 基础功能与驱动版本无关,协议命令专项与驱动版本相关(缺陷在服务端)" POSITION="right"/>
62+</node>
63+<node TEXT="结论" POSITION="right">
64+<node TEXT="Boot 3.x + 新驱动基础 CRUD / 事务 / 批量 / 类型映射 / 连接池可用" POSITION="right"/>
65+<node TEXT="服务端预编译 + SEND_LONG_DATA/RESET 场景建议 9.7.0 及以上驱动(FINDING H/I)" POSITION="right"/>
66+<node TEXT="支撑 docs 仓版本范围表述更新:8.0.28 及以上均可正常工作" POSITION="right"/>
67+</node>
68+</node>
69+</map>
@@ -0,0 +1,133 @@
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>springboot-connect-opengauss-b</artifactId>
16+ <version>1.0.0</version>
17+ <name>springboot-connect-opengauss-b</name>
18+ <description>Spring Boot(JDBC) 连接 openGauss B 兼容库 — Issue #27</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-jdbc</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+ <!-- MySQL JDBC 驱动按 profile 切换,用于版本扫测矩阵。
37+ 注意:8.0.28 仅存在于旧坐标 mysql:mysql-connector-java;
38+ 8.0.31 起(含 8.0.33/8.1.0/8.3.0/8.4.0/9.7.0/26.7.0)使用新坐标 com.mysql:mysql-connector-j。 -->
39+ <profiles>
40+ <!-- 默认档:新坐标,版本交给 Boot BOM 管理(3.5.16 默认管理 9.x) -->
41+ <profile>
42+ <id>driver-boot-default</id>
43+ <activation>
44+ <activeByDefault>true</activeByDefault>
45+ </activation>
46+ <dependencies>
47+ <dependency>
48+ <groupId>com.mysql</groupId>
49+ <artifactId>mysql-connector-j</artifactId>
50+ <scope>runtime</scope>
51+ </dependency>
52+ </dependencies>
53+ </profile>
54+ <!-- 文档基准档:8.0.28(开发指导文档支持上限) -->
55+ <profile>
56+ <id>driver-8.0.28</id>
57+ <dependencies>
58+ <dependency>
59+ <groupId>mysql</groupId>
60+ <artifactId>mysql-connector-java</artifactId>
61+ <version>8.0.28</version>
62+ <scope>runtime</scope>
63+ </dependency>
64+ </dependencies>
65+ </profile>
66+ <profile>
67+ <id>driver-8.0.33</id>
68+ <dependencies>
69+ <dependency>
70+ <groupId>com.mysql</groupId>
71+ <artifactId>mysql-connector-j</artifactId>
72+ <version>8.0.33</version>
73+ <scope>runtime</scope>
74+ </dependency>
75+ </dependencies>
76+ </profile>
77+ <profile>
78+ <id>driver-8.1.0</id>
79+ <dependencies>
80+ <dependency>
81+ <groupId>com.mysql</groupId>
82+ <artifactId>mysql-connector-j</artifactId>
83+ <version>8.1.0</version>
84+ <scope>runtime</scope>
85+ </dependency>
86+ </dependencies>
87+ </profile>
88+ <profile>
89+ <id>driver-8.3.0</id>
90+ <dependencies>
91+ <dependency>
92+ <groupId>com.mysql</groupId>
93+ <artifactId>mysql-connector-j</artifactId>
94+ <version>8.3.0</version>
95+ <scope>runtime</scope>
96+ </dependency>
97+ </dependencies>
98+ </profile>
99+ <profile>
100+ <id>driver-8.4.0</id>
101+ <dependencies>
102+ <dependency>
103+ <groupId>com.mysql</groupId>
104+ <artifactId>mysql-connector-j</artifactId>
105+ <version>8.4.0</version>
106+ <scope>runtime</scope>
107+ </dependency>
108+ </dependencies>
109+ </profile>
110+ <profile>
111+ <id>driver-9.7.0</id>
112+ <dependencies>
113+ <dependency>
114+ <groupId>com.mysql</groupId>
115+ <artifactId>mysql-connector-j</artifactId>
116+ <version>9.7.0</version>
117+ <scope>runtime</scope>
118+ </dependency>
119+ </dependencies>
120+ </profile>
121+ <profile>
122+ <id>driver-26.7.0</id>
123+ <dependencies>
124+ <dependency>
125+ <groupId>com.mysql</groupId>
126+ <artifactId>mysql-connector-j</artifactId>
127+ <version>26.7.0</version>
128+ <scope>runtime</scope>
129+ </dependency>
130+ </dependencies>
131+ </profile>
132+ </profiles>
133+</project>
@@ -0,0 +1,50 @@
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.jdbc.core.JdbcTemplate;
7+import org.springframework.stereotype.Component;
8+ 
9+/**
10+ * 冒烟程序:验证 MySQL JDBC -> dolphin 协议 -> B 库 全链路。
11+ * 启动前确认:openGauss 已开启 enable_dolphin_proto,B 库/schema/user 表已按 README 建好。
12+ */
13+@SpringBootApplication
14+public class SmokeApplication {
15+ 
16+ public static void main(String[] args) {
17+ SpringApplication.run(SmokeApplication.class, args);
18+ }
19+ 
20+ @Component
21+ public static class SmokeRunner implements CommandLineRunner {
22+ 
23+ private final JdbcTemplate jdbc;
24+ 
25+ public SmokeRunner(JdbcTemplate jdbc) {
26+ this.jdbc = jdbc;
27+ }
28+ 
29+ @Override
30+ public void run(String... args) {
31+ // 1. 连接与查询
32+ Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM `user`", Integer.class);
33+ System.out.println("[SMOKE] 连接成功,`user` 表行数 = " + count);
34+ 
35+ // 2. 插入
36+ jdbc.update("INSERT INTO `user` (`name`, `age`) VALUES (?, ?)", "smoke", 99);
37+ Integer age = jdbc.queryForObject(
38+ "SELECT `age` FROM `user` WHERE `name` = ?", Integer.class, "smoke");
39+ System.out.println("[SMOKE] 插入成功,age = " + age);
40+ 
41+ // 3. 更新
42+ jdbc.update("UPDATE `user` SET `age` = ? WHERE `name` = ?", 100, "smoke");
43+ System.out.println("[SMOKE] 更新成功");
44+ 
45+ // 4. 删除
46+ jdbc.update("DELETE FROM `user` WHERE `name` = ?", "smoke");
47+ System.out.println("[SMOKE] 删除成功,CRUD 全链路通过");
48+ }
49+ }
50+}
@@ -0,0 +1,22 @@
1+package org.opengauss.smoke;
2+ 
3+import org.springframework.jdbc.core.JdbcTemplate;
4+import org.springframework.stereotype.Service;
5+import org.springframework.transaction.annotation.Transactional;
6+ 
7+/** 事务行为验证用服务:@Transactional 提交/回滚由 Spring 事务管理驱动。 */
8+@Service
9+public class TxService {
10+ 
11+ private final JdbcTemplate jdbc;
12+ 
13+ public TxService(JdbcTemplate jdbc) {
14+ this.jdbc = jdbc;
15+ }
16+ 
17+ @Transactional
18+ public void insertThenThrow(String name, int age) {
19+ jdbc.update("INSERT INTO `user` (`name`, `age`) VALUES (?, ?)", name, age);
20+ throw new RuntimeException("事务回滚验证");
21+ }
22+}
@@ -0,0 +1,12 @@
1+spring:
2+ datasource:
3+ # DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORD 通过环境变量或 -D 覆盖
4+ # 端口是 dolphin 协议端口(dolphin_server_port,默认 3308),不是 openGauss 原生端口 5432
5+ url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3308}/${DB_NAME:mysql_test_db}?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8
6+ username: ${DB_USER:mysql_test_db}
7+ password: ${DB_PASSWORD:ChangeMe_123}
8+ # 8.0.x 新旧坐标驱动类名相同
9+ driver-class-name: com.mysql.cj.jdbc.Driver
10+ hikari:
11+ # 冒烟阶段不配置,测试连接池行为时再按用例调整
12+ maximum-pool-size: 10
@@ -0,0 +1,97 @@
1+package org.opengauss.cases;
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.sql.Connection;
8+import java.sql.PreparedStatement;
9+import java.sql.SQLException;
10+ 
11+import static org.junit.jupiter.api.Assertions.assertEquals;
12+import static org.opengauss.cases.DbUtil.*;
13+ 
14+/**
15+ * 维度三:DML 批量。
16+ * PreparedStatement 批量、rewriteBatchedStatements 开关对比(计时只记录不做硬断言)、
17+ * Spring JdbcTemplate 批量。
18+ */
19+class BatchDmlTest {
20+ 
21+ @BeforeAll
22+ static void createTable() throws SQLException {
23+ try (Connection c = raw(null)) {
24+ exec(c,
25+ "DROP TABLE IF EXISTS t_batch",
26+ "CREATE TABLE t_batch (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), age INT)");
27+ }
28+ }
29+ 
30+ @Test
31+ @DisplayName("3.1 PreparedStatement 批量插入 1000 行")
32+ void preparedBatch1000() throws SQLException {
33+ try (Connection c = raw(null)) {
34+ try (PreparedStatement ps = c.prepareStatement(
35+ "INSERT INTO t_batch (name, age) VALUES (?, ?)")) {
36+ for (int i = 1; i <= 1000; i++) {
37+ ps.setString(1, "batch_" + i);
38+ ps.setInt(2, i);
39+ ps.addBatch();
40+ }
41+ int[] counts = ps.executeBatch();
42+ int sum = 0;
43+ for (int n : counts) {
44+ sum += n;
45+ }
46+ log("3.1 executeBatch 返回 %d 条计数,合计 %d(预期 1000)", counts.length, sum);
47+ assertEquals(1000, sum, "批量插入应有 1000 行受影响");
48+ }
49+ }
50+ }
51+ 
52+ @Test
53+ @DisplayName("3.2 rewriteBatchedStatements 开关对比(500 行 × 2 轮计时)")
54+ void rewriteBatchCompare() throws SQLException {
55+ for (String extra : new String[]{null, "rewriteBatchedStatements=true"}) {
56+ long start = System.nanoTime();
57+ try (Connection c = raw(extra)) {
58+ try (PreparedStatement ps = c.prepareStatement(
59+ "INSERT INTO t_batch (name, age) VALUES (?, ?)")) {
60+ for (int i = 1; i <= 500; i++) {
61+ ps.setString(1, "rw_" + (extra == null ? "unset" : "on") + "_" + i);
62+ ps.setInt(2, i);
63+ ps.addBatch();
64+ }
65+ ps.executeBatch();
66+ }
67+ }
68+ long ms = (System.nanoTime() - start) / 1_000_000;
69+ log("3.2 rewriteBatchedStatements=%s 插入 500 行耗时 %d ms",
70+ extra == null ? "未显式设置" : "true", ms);
71+ }
72+ }
73+ 
74+ @Test
75+ @DisplayName("3.3 JdbcTemplate 批量(Spring 层面)")
76+ void jdbcTemplateBatch() {
77+ var jdbc = new org.springframework.jdbc.core.JdbcTemplate(
78+ new org.springframework.jdbc.datasource.SingleConnectionDataSource(
79+ url(null), USER, PASSWORD, true));
80+ int[] rows = jdbc.batchUpdate(
81+ "INSERT INTO t_batch (name, age) VALUES (?, ?)",
82+ new org.springframework.jdbc.core.BatchPreparedStatementSetter() {
83+ @Override
84+ public void setValues(java.sql.PreparedStatement ps, int i) throws SQLException {
85+ ps.setString(1, "jt_" + i);
86+ ps.setInt(2, i);
87+ }
88+ 
89+ @Override
90+ public int getBatchSize() {
91+ return 5;
92+ }
93+ });
94+ log("3.3 JdbcTemplate batchUpdate 返回 %d 条计数", rows.length);
95+ assertEquals(5, rows.length);
96+ }
97+}
@@ -0,0 +1,74 @@
1+package org.opengauss.cases;
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.beans.factory.annotation.Autowired;
7+import org.springframework.boot.test.context.SpringBootTest;
8+ 
9+import javax.sql.DataSource;
10+import java.sql.Connection;
11+import java.sql.DatabaseMetaData;
12+import java.sql.SQLException;
13+ 
14+import static org.junit.jupiter.api.Assertions.assertNotNull;
15+import static org.junit.jupiter.api.Assertions.assertThrows;
16+import static org.opengauss.cases.DbUtil.*;
17+ 
18+/**
19+ * 维度一:连接与认证。
20+ * 验证 dolphin 协议下的认证路径(native 密码)、错误凭据拒绝、
21+ * 端口区分(3308 协议口 vs 5432 原生口)、连接元数据返回。
22+ */
23+@SpringBootTest(classes = SmokeApplication.class)
24+class ConnectionAuthTest {
25+ 
26+ @Autowired
27+ private DataSource dataSource;
28+ 
29+ @Test
30+ @DisplayName("1.1 正常连接并读取数据库产品元数据")
31+ void connectAndMetaData() throws SQLException {
32+ try (Connection conn = dataSource.getConnection()) {
33+ DatabaseMetaData md = conn.getMetaData();
34+ log("1.1 连接成功");
35+ log("1.1 productName=%s", md.getDatabaseProductName());
36+ log("1.1 productVersion=%s", md.getDatabaseProductVersion());
37+ log("1.1 driver=%s %s", md.getDriverName(), md.getDriverVersion());
38+ // getTables 是 ORM/框架启动时的必经元数据调用;B 模式行为实测记录
39+ try {
40+ boolean hasUser = md.getTables(DB, null, "user", null).next();
41+ log("1.1 getTables(user) 可用,user 表存在=%s", hasUser);
42+ } catch (SQLException e) {
43+ log("1.1 FINDING: getTables 报错: %s -> 依赖 information_schema.TABLES"
44+ + " 的 JDBC 元数据 API 在 B 模式下不可用", e.getMessage());
45+ }
46+ }
47+ }
48+ 
49+ @Test
50+ @DisplayName("1.2 错误密码应拒绝连接")
51+ void wrongPassword() {
52+ SQLException ex = assertThrows(SQLException.class,
53+ () -> raw(null, USER, PASSWORD + "_wrong").close());
54+ log("1.2 错误密码被拒绝: SQLState=%s, msg=%s", ex.getSQLState(), ex.getMessage());
55+ }
56+ 
57+ @Test
58+ @DisplayName("1.3 错误用户名应拒绝连接")
59+ void wrongUser() {
60+ SQLException ex = assertThrows(SQLException.class,
61+ () -> raw(null, USER + "_no", PASSWORD).close());
62+ log("1.3 错误用户名被拒绝: SQLState=%s, msg=%s", ex.getSQLState(), ex.getMessage());
63+ }
64+ 
65+ @Test
66+ @DisplayName("1.4 MySQL 驱动连 5432 原生端口应失败(协议口是 3308)")
67+ void wrongPort() {
68+ String nativeUrl = "jdbc:mysql://" + HOST + ":5432/" + DB
69+ + "?useSSL=false&connectTimeout=3000";
70+ SQLException ex = assertThrows(SQLException.class,
71+ () -> java.sql.DriverManager.getConnection(nativeUrl, USER, PASSWORD).close());
72+ log("1.4 连 5432 被拒: SQLState=%s, msg=%s", ex.getSQLState(), ex.getMessage());
73+ }
74+}
@@ -0,0 +1,76 @@
1+package org.opengauss.cases;
2+ 
3+import com.zaxxer.hikari.HikariConfig;
4+import com.zaxxer.hikari.HikariDataSource;
5+import org.junit.jupiter.api.DisplayName;
6+import org.junit.jupiter.api.Test;
7+import org.opengauss.smoke.SmokeApplication;
8+import org.springframework.beans.factory.annotation.Autowired;
9+import org.springframework.boot.test.context.SpringBootTest;
10+ 
11+import javax.sql.DataSource;
12+import java.sql.Connection;
13+import java.sql.PreparedStatement;
14+import java.sql.ResultSet;
15+import java.sql.SQLException;
16+ 
17+import static org.junit.jupiter.api.Assertions.assertEquals;
18+import static org.junit.jupiter.api.Assertions.assertTrue;
19+import static org.opengauss.cases.DbUtil.*;
20+ 
21+/**
22+ * 维度五:连接池(HikariCP)。
23+ * Spring 配置生效性;用 CONNECTION_ID() 验证连接复用(池大小=1 时两次取连接应同 id)。
24+ * 若 B 模式不支持 CONNECTION_ID(),记录为 FINDING 并跳过复用断言。
25+ */
26+@SpringBootTest(classes = SmokeApplication.class)
27+class ConnectionPoolTest {
28+ 
29+ @Autowired
30+ private DataSource dataSource;
31+ 
32+ @Test
33+ @DisplayName("5.1 application.yml 的 Hikari 配置生效")
34+ void springConfigApplied() {
35+ assertTrue(dataSource instanceof HikariDataSource, "Boot 默认连接池应为 HikariCP");
36+ HikariDataSource hds = (HikariDataSource) dataSource;
37+ log("5.1 连接池类型=%s, maximumPoolSize=%d(yml 配置 10)",
38+ hds.getClass().getSimpleName(), hds.getMaximumPoolSize());
39+ assertEquals(10, hds.getMaximumPoolSize());
40+ }
41+ 
42+ @Test
43+ @DisplayName("5.2 CONNECTION_ID() 验证连接复用(独立池,池大小=1)")
44+ void connectionReuse() throws SQLException {
45+ HikariConfig config = new HikariConfig();
46+ config.setJdbcUrl(url(null));
47+ config.setUsername(USER);
48+ config.setPassword(PASSWORD);
49+ config.setDriverClassName("com.mysql.cj.jdbc.Driver");
50+ config.setMaximumPoolSize(1);
51+ 
52+ try (HikariDataSource ds = new HikariDataSource(config)) {
53+ long id1;
54+ try (Connection c = ds.getConnection()) {
55+ id1 = connectionId(c);
56+ } catch (SQLException e) {
57+ log("5.2 FINDING: CONNECTION_ID() 不可用: %s -> 改用其他方式验证复用", e.getMessage());
58+ return;
59+ }
60+ long id2;
61+ try (Connection c = ds.getConnection()) {
62+ id2 = connectionId(c);
63+ }
64+ log("5.2 第一次连接 CONNECTION_ID=%d,第二次=%d(池大小=1)", id1, id2);
65+ assertEquals(id1, id2, "池大小=1 时两次取连接应为同一物理连接");
66+ }
67+ }
68+ 
69+ private long connectionId(Connection c) throws SQLException {
70+ try (PreparedStatement ps = c.prepareStatement("SELECT CONNECTION_ID()");
71+ ResultSet rs = ps.executeQuery()) {
72+ rs.next();
73+ return rs.getLong(1);
74+ }
75+ }
76+}
@@ -0,0 +1,54 @@
1+package org.opengauss.cases;
2+ 
3+import java.sql.Connection;
4+import java.sql.DriverManager;
5+import java.sql.SQLException;
6+import java.sql.Statement;
7+ 
8+/** 六维度用例公共工具:从环境变量取连接信息、按参数拼 JDBC URL、统一打日志。 */
9+public final class DbUtil {
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 DbUtil() {
18+ }
19+ 
20+ /** 拼 MySQL JDBC URL;extraParams 形如 "tinyInt1isBit=false&...",null 表示不带额外参数。 */
21+ public static String url(String extraParams) {
22+ String base = "jdbc:mysql://" + HOST + ":" + PORT + "/" + DB
23+ + "?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8";
24+ return extraParams == null || extraParams.isEmpty() ? base : base + "&" + extraParams;
25+ }
26+ 
27+ /** 原生连接(不走 Spring 连接池),用于驱动参数行为专项。 */
28+ public static Connection raw(String extraParams) throws SQLException {
29+ return DriverManager.getConnection(url(extraParams), USER, PASSWORD);
30+ }
31+ 
32+ public static Connection raw(String extraParams, String user, String password) throws SQLException {
33+ return DriverManager.getConnection(url(extraParams), user, password);
34+ }
35+ 
36+ /** 逐条执行 DDL。 */
37+ public static void exec(Connection conn, String... sqls) throws SQLException {
38+ try (Statement st = conn.createStatement()) {
39+ for (String sql : sqls) {
40+ st.execute(sql);
41+ }
42+ }
43+ }
44+ 
45+ /** 统一日志前缀:[CASE] 开头的行会被整理进自测报告。 */
46+ public static void log(String fmt, Object... args) {
47+ System.out.println("[CASE] " + String.format(fmt, args));
48+ }
49+ 
50+ private static String env(String key, String def) {
51+ String v = System.getenv(key);
52+ return (v == null || v.isEmpty()) ? def : v;
53+ }
54+}
@@ -0,0 +1,102 @@
1+package org.opengauss.cases;
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.sql.CallableStatement;
8+import java.sql.Connection;
9+import java.sql.PreparedStatement;
10+import java.sql.ResultSet;
11+import java.sql.SQLException;
12+import java.sql.Statement;
13+ 
14+import static org.junit.jupiter.api.Assertions.assertEquals;
15+import static org.junit.jupiter.api.Assertions.assertTrue;
16+import static org.opengauss.cases.DbUtil.*;
17+ 
18+/**
19+ * 维度六:文档明示的已知限制专项。
20+ * 6.1 游标(useCursorFetch/setFetchSize 文档标注不支持)
21+ * 6.2 存储过程(CallableStatement 文档标注不支持)
22+ * 实测行为并记录——限制可能表现为报错、静默忽略或已支持。
23+ */
24+class KnownLimitsTest {
25+ 
26+ @BeforeAll
27+ static void createData() throws SQLException {
28+ try (Connection c = raw(null)) {
29+ exec(c,
30+ "DROP TABLE IF EXISTS t_cursor",
31+ "CREATE TABLE t_cursor (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), age INT)");
32+ try (PreparedStatement ps = c.prepareStatement(
33+ "INSERT INTO t_cursor (name, age) VALUES (?, ?)")) {
34+ for (int i = 1; i <= 2000; i++) {
35+ ps.setString(1, "cursor_" + i);
36+ ps.setInt(2, i);
37+ ps.addBatch();
38+ }
39+ ps.executeBatch();
40+ }
41+ log("6.0 准备 2000 行游标测试数据(t_cursor)");
42+ }
43+ }
44+ 
45+ @Test
46+ @DisplayName("6.1 游标:useCursorFetch=true + useServerPrepStmts=true + setFetchSize")
47+ void cursorFetch() throws SQLException {
48+ try (Connection c = raw("useCursorFetch=true&useServerPrepStmts=true")) {
49+ try (PreparedStatement ps = c.prepareStatement(
50+ "SELECT id, name FROM t_cursor ORDER BY id")) {
51+ ps.setFetchSize(100);
52+ try (ResultSet rs = ps.executeQuery()) {
53+ int n = 0;
54+ while (rs.next()) {
55+ n++;
56+ }
57+ log("6.1 游标查询无报错,完整读取 %d 行。文档标注不支持游标,"
58+ + "此处无报错不代表服务端逐批返回(可能静默退化为全量拉取),"
59+ + "需协议层确认,报告中按'驱动层可用、协议层待确认'记录", n);
60+ assertEquals(2000, n);
61+ }
62+ } catch (SQLException e) {
63+ log("6.1 游标查询报错(符合文档'不支持游标'的预期): SQLState=%s, msg=%s",
64+ e.getSQLState(), e.getMessage());
65+ }
66+ }
67+ }
68+ 
69+ @Test
70+ @DisplayName("6.2 游标:普通 Statement setFetchSize(驱动可能静默忽略)")
71+ void cursorFetchPlainStatement() throws SQLException {
72+ try (Connection c = raw(null)) {
73+ try (Statement st = c.createStatement()) {
74+ st.setFetchSize(50);
75+ try (ResultSet rs = st.executeQuery("SELECT id FROM t_cursor ORDER BY id")) {
76+ int n = 0;
77+ while (rs.next()) {
78+ n++;
79+ }
80+ log("6.2 普通 Statement setFetchSize(50) 正常完成,读取 %d 行", n);
81+ assertEquals(2000, n);
82+ }
83+ }
84+ }
85+ }
86+ 
87+ @Test
88+ @DisplayName("6.3 存储过程:CallableStatement 应报错(文档标注不支持)")
89+ void callableStatement() {
90+ SQLException ex = null;
91+ try (Connection c = raw(null)) {
92+ try (CallableStatement cs = c.prepareCall("{call no_such_proc()}")) {
93+ cs.execute();
94+ }
95+ } catch (SQLException e) {
96+ ex = e;
97+ }
98+ assertTrue(ex != null, "存储过程调用应报错(文档标注不支持)");
99+ log("6.3 CallableStatement 报错(符合文档预期): SQLState=%s, msg=%s",
100+ ex.getSQLState(), ex.getMessage());
101+ }
102+}
@@ -0,0 +1,277 @@
1+package org.opengauss.cases;
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.cases.DbUtil.*;
19+ 
20+/**
21+ * 维度七:MySQL 协议命令基础功能专项(评审意见:COM_STMT_PREPARE / COM_STMT_SEND_LONG_DATA /
22+ * COM_STMT_EXECUTE 最好覆盖到每个 command)。
23+ * 按"JDBC 是否有触发路径"逐条实测驱动可触发的全部命令:
24+ * 7.1 COM_QUERY(普通 Statement,文本协议)
25+ * 7.2 COM_INIT_DB(连接建立携带 database;setCatalog + useLocalSessionState=false
26+ * 7.3 COM_PING(isValid)
27+ * 7.4 COM_STMT_PREPARE / COM_STMT_EXECUTE / COM_STMT_CLOSE(服务端预编译)
28+ * 7.5 COM_STMT_SEND_LONG_DATA(服务端预编译 + 1MB LONGBLOB 流式写入)
29+ * 7.6 COM_STMT_RESET(clearParameters 后复用同一预编译语句)
30+ * 7.7 COM_STMT_FETCH(useCursorFetch + setFetchSize)
31+ * 7.8 COM_SET_OPTION(allowMultiQueries 多语句)
32+ * 7.9 COM_QUIT(close)
33+ * COM_FIELD_LIST / COM_PROCESS_INFO / COM_STATISTICS / COM_PROCESS_KILL / COM_CHANGE_USER
34+ * 等命令 JDBC 驱动无触发路径(不提供对应 API),在自测报告 4.1.8 说明,不在此处用例化。
35+ */
36+class ProtocolCommandTest {
37+ 
38+ @BeforeAll
39+ static void createData() throws SQLException {
40+ try (Connection c = raw(null)) {
41+ exec(c,
42+ "DROP TABLE IF EXISTS t_cmd",
43+ "CREATE TABLE t_cmd (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), data LONGBLOB)");
44+ try (PreparedStatement ps = c.prepareStatement(
45+ "INSERT INTO t_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("7.0 准备协议命令测试数据(t_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 t_cmd")) {
59+ rs.next();
60+ return rs.getInt(1);
61+ }
62+ }
63+ 
64+ @Test
65+ @DisplayName("7.1 COM_QUERY:普通 Statement 执行 SQL(文本协议)")
66+ void comQuery() throws SQLException {
67+ try (Connection c = raw(null)) {
68+ int before = count(c);
69+ try (Statement st = c.createStatement()) {
70+ st.executeUpdate("INSERT INTO t_cmd (name) VALUES ('query_a')");
71+ st.executeUpdate("INSERT INTO t_cmd (name) VALUES ('query_b')");
72+ }
73+ int after = count(c);
74+ log("7.1 COM_QUERY 文本协议插入/查询正常:%d -> %d 行", before, after);
75+ assertEquals(before + 2, after);
76+ }
77+ }
78+ 
79+ @Test
80+ @DisplayName("7.2 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("7.2 COM_INIT_DB 连接初始化/切换 database 正常:catalog=%s", c.getCatalog());
89+ }
90+ }
91+ 
92+ @Test
93+ @DisplayName("7.3 COM_PING:Connection.isValid")
94+ void comPing() throws SQLException {
95+ try (Connection c = raw(null)) {
96+ boolean ok = c.isValid(2);
97+ log("7.3 COM_PING 心跳正常:isValid=%s", ok);
98+ assertTrue(ok);
99+ }
100+ }
101+ 
102+ @Test
103+ @DisplayName("7.4 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 t_cmd (name) VALUES (?)")) {
109+ ps.setString(1, "prep_7_4");
110+ assertEquals(1, ps.executeUpdate());
111+ } // 语句 close -> COM_STMT_CLOSE
112+ try (PreparedStatement ps = c.prepareStatement(
113+ "SELECT name FROM t_cmd WHERE name = ?")) {
114+ ps.setString(1, "prep_7_4");
115+ try (ResultSet rs = ps.executeQuery()) {
116+ assertTrue(rs.next(), "服务端预编译语句执行后应查回已插入行");
117+ assertEquals("prep_7_4", rs.getString(1));
118+ }
119+ }
120+ log("7.4 COM_STMT_PREPARE/EXECUTE/CLOSE 服务端预编译插入+查询往返正常");
121+ }
122+ }
123+ 
124+ @Test
125+ @DisplayName("7.5 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 t_cmd (name, data) VALUES (?, ?)")) {
134+ ps.setString(1, "long_7_5");
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 t_cmd WHERE name = ?")) {
141+ ps.setString(1, "long_7_5");
142+ try (ResultSet rs = ps.executeQuery()) {
143+ assertTrue(rs.next());
144+ read = rs.getBytes(1);
145+ }
146+ }
147+ log("7.5a 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+ // (MySQL 原生完整存储;dolphin 在 SEND_LONG_DATA 二进制参数处理中遇 0x00 截断,
154+ // 客户端预编译文本协议路径无此问题)。服务端修复后本断言自动失败,
155+ // 提醒移除规避说明并更新自测报告。
156+ byte[] mixed = new byte[200];
157+ Arrays.fill(mixed, (byte) 0x41);
158+ mixed[150] = 0x00;
159+ Arrays.fill(mixed, 151, mixed.length, (byte) 0x42);
160+ try (PreparedStatement ps = c.prepareStatement(
161+ "INSERT INTO t_cmd (name, data) VALUES (?, ?)")) {
162+ ps.setString(1, "long_7_5_mixed");
163+ ps.setBlob(2, new ByteArrayInputStream(mixed));
164+ assertEquals(1, ps.executeUpdate());
165+ }
166+ byte[] mixedRead;
167+ try (PreparedStatement ps = c.prepareStatement(
168+ "SELECT data FROM t_cmd WHERE name = ?")) {
169+ ps.setString(1, "long_7_5_mixed");
170+ try (ResultSet rs = ps.executeQuery()) {
171+ assertTrue(rs.next());
172+ mixedRead = rs.getBytes(1);
173+ }
174+ }
175+ log("7.5b FINDING F:含 0x00 的 200 字节读回 %d 字节(=第一个 0x00 前的前缀长度,MySQL 原生应回 200)",
176+ mixedRead.length);
177+ assertEquals(150, mixedRead.length,
178+ "FINDING F 回归断言:dolphin 遇 0x00 截断为前缀;服务端修复后应回 200 字节");
179+ }
180+ }
181+ 
182+ @Test
183+ @DisplayName("7.6 COM_STMT_RESET:clearParameters 后复用同一预编译语句")
184+ void comStmtReset() throws SQLException {
185+ try (Connection c = raw("useServerPrepStmts=true")) {
186+ int before = count(c);
187+ try (PreparedStatement ps = c.prepareStatement(
188+ "INSERT INTO t_cmd (name) VALUES (?)")) {
189+ ps.setString(1, "reset_a");
190+ assertEquals(1, ps.executeUpdate());
191+ ps.clearParameters(); // 参数置空,下次执行时驱动发 COM_STMT_RESET
192+ ps.setString(1, "reset_b");
193+ assertEquals(1, ps.executeUpdate());
194+ }
195+ int after = count(c);
196+ log("7.6 COM_STMT_RESET clearParameters 后复用预编译语句正常:%d -> %d 行", before, after);
197+ assertEquals(before + 2, after);
198+ }
199+ }
200+ 
201+ @Test
202+ @DisplayName("7.7 COM_STMT_FETCH:useCursorFetch + setFetchSize")
203+ void comStmtFetch() throws SQLException {
204+ try (Connection c = raw("useCursorFetch=true&useServerPrepStmts=true")) {
205+ int n;
206+ try (PreparedStatement ps = c.prepareStatement(
207+ "SELECT id, name FROM t_cmd ORDER BY id")) {
208+ ps.setFetchSize(100);
209+ try (ResultSet rs = ps.executeQuery()) {
210+ n = 0;
211+ while (rs.next()) {
212+ n++;
213+ }
214+ }
215+ }
216+ log("7.7 COM_STMT_FETCH 游标拉取无报错,读取 %d 行。协议层是否真逐批返回需服务端日志佐证"
217+ + "(同 6.1 口径,报告中按'驱动层可用、协议层待确认'记录)", n);
218+ assertTrue(n >= 500, "应至少读回准备阶段写入的 500 行");
219+ }
220+ }
221+ 
222+ @Test
223+ @DisplayName("7.8 COM_SET_OPTION:allowMultiQueries=true 多语句执行")
224+ void comSetOption() throws SQLException {
225+ try (Connection c = raw("allowMultiQueries=true")) {
226+ int before = count(c);
227+ int cnt = -1;
228+ boolean hasResults;
229+ try (Statement st = c.createStatement()) {
230+ hasResults = st.execute("INSERT INTO t_cmd (name) VALUES ('mq_1'); "
231+ + "INSERT INTO t_cmd (name) VALUES ('mq_2'); "
232+ + "SELECT COUNT(*) FROM t_cmd");
233+ // FINDING G:dolphin 多语句结果返回结构不完整——execute() 只反映首语句
234+ // 结果(首语句为 DML 时返回 false + 首语句 update count),getMoreResults()
235+ // 取不到后续语句的结果集(B 组诊断:首结果 + 末 update count 之外全部丢弃;
236+ // 纯 SELECT 多结果集则正常逐个返回)。MySQL 原生按语句逐个返回全部结果。
237+ // 服务端修复后 more=true 且 cnt 能读到 COUNT(*),本断言自动失败提醒更新报告。
238+ hasResults = st.getMoreResults();
239+ if (hasResults) {
240+ try (ResultSet rs = st.getResultSet()) {
241+ if (rs.next()) {
242+ cnt = rs.getInt(1);
243+ }
244+ }
245+ }
246+ }
247+ int after = count(c);
248+ log("7.8 COM_SET_OPTION 多语句执行生效:%d -> %d 行(后续 SELECT 结果集经 getMoreResults "
249+ + "不可得,见 FINDING G)", before, after);
250+ assertEquals(before + 2, after, "多语句中的 DML 应全部生效");
251+ assertEquals(-1, cnt,
252+ "FINDING G 回归断言:dolphin 当前不回传后续 SELECT 结果集;服务端修复后 cnt 应=落库行数");
253+ }
254+ }
255+ 
256+ @Test
257+ @DisplayName("7.9 COM_QUIT:Connection.close")
258+ void comQuit() throws SQLException {
259+ Connection c = raw(null);
260+ c.close();
261+ assertTrue(c.isClosed());
262+ log("7.9 COM_QUIT 连接关闭正常(isClosed=true)");
263+ }
264+ 
265+ private static String md5(byte[] data) {
266+ try {
267+ byte[] digest = MessageDigest.getInstance("MD5").digest(data);
268+ StringBuilder sb = new StringBuilder();
269+ for (byte b : digest) {
270+ sb.append(String.format("%02x", b));
271+ }
272+ return sb.toString();
273+ } catch (Exception e) {
274+ throw new IllegalStateException(e);
275+ }
276+ }
277+}
@@ -0,0 +1,123 @@
1+package org.opengauss.cases;
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.beans.factory.annotation.Autowired;
7+import org.springframework.boot.test.context.SpringBootTest;
8+import org.springframework.jdbc.core.JdbcTemplate;
9+import org.springframework.transaction.support.TransactionTemplate;
10+ 
11+import java.sql.Connection;
12+import java.sql.SQLException;
13+import java.sql.Savepoint;
14+ 
15+import static org.junit.jupiter.api.Assertions.assertEquals;
16+import static org.opengauss.cases.DbUtil.*;
17+ 
18+/**
19+ * 维度四:事务。
20+ * Spring TransactionTemplate 提交/回滚、手动 Connection 事务、保存点、隔离级别接受性。
21+ * 复用冒烟环境的 `user` 表,测试数据前缀 t_tx_,用例结束自行清理。
22+ */
23+@SpringBootTest(classes = SmokeApplication.class)
24+class TransactionTest {
25+ 
26+ @Autowired
27+ private JdbcTemplate jdbc;
28+ 
29+ @Autowired
30+ private TransactionTemplate txTemplate;
31+ 
32+ private int count(String prefix) {
33+ Integer n = jdbc.queryForObject(
34+ "SELECT COUNT(*) FROM `user` WHERE `name` LIKE ?", Integer.class, prefix + "%");
35+ return n == null ? 0 : n;
36+ }
37+ 
38+ private void cleanup(String prefix) {
39+ jdbc.update("DELETE FROM `user` WHERE `name` LIKE ?", prefix + "%");
40+ }
41+ 
42+ @Test
43+ @DisplayName("4.1 TransactionTemplate:提交后对外可见")
44+ void commitVisible() {
45+ txTemplate.execute(status -> {
46+ jdbc.update("INSERT INTO `user` (`name`, `age`) VALUES (?, ?)", "t_tx_commit", 1);
47+ return null;
48+ });
49+ assertEquals(1, count("t_tx_commit"), "提交后应可见");
50+ cleanup("t_tx_commit");
51+ }
52+ 
53+ @Test
54+ @DisplayName("4.2 TransactionTemplate:setRollbackOnly 回滚后不可见")
55+ void rollbackInvisible() {
56+ txTemplate.execute(status -> {
57+ jdbc.update("INSERT INTO `user` (`name`, `age`) VALUES (?, ?)", "t_tx_rollback", 1);
58+ status.setRollbackOnly();
59+ return null;
60+ });
61+ assertEquals(0, count("t_tx_rollback"), "回滚后应不可见");
62+ cleanup("t_tx_rollback");
63+ }
64+ 
65+ @Test
66+ @DisplayName("4.3 手动 Connection 事务:commit / rollback")
67+ void manualConnectionTx() throws SQLException {
68+ try (Connection c = raw(null)) {
69+ c.setAutoCommit(false);
70+ try (var st = c.createStatement()) {
71+ st.executeUpdate("INSERT INTO `user` (`name`, `age`) VALUES ('t_tx_manual', 1)");
72+ }
73+ c.rollback();
74+ assertEquals(0, count("t_tx_manual"), "rollback 后不可见");
75+ 
76+ try (var st = c.createStatement()) {
77+ st.executeUpdate("INSERT INTO `user` (`name`, `age`) VALUES ('t_tx_manual', 1)");
78+ }
79+ c.commit();
80+ }
81+ assertEquals(1, count("t_tx_manual"), "commit 后可见");
82+ cleanup("t_tx_manual");
83+ }
84+ 
85+ @Test
86+ @DisplayName("4.4 保存点:部分回滚")
87+ void savepoint() throws SQLException {
88+ try (Connection c = raw(null)) {
89+ c.setAutoCommit(false);
90+ try (var st = c.createStatement()) {
91+ st.executeUpdate("INSERT INTO `user` (`name`, `age`) VALUES ('t_tx_sp_a', 1)");
92+ }
93+ Savepoint sp = c.setSavepoint("sp1");
94+ try (var st = c.createStatement()) {
95+ st.executeUpdate("INSERT INTO `user` (`name`, `age`) VALUES ('t_tx_sp_b', 1)");
96+ }
97+ c.rollback(sp);
98+ c.commit();
99+ }
100+ assertEquals(1, count("t_tx_sp_a"), "保存点之前的数据应保留");
101+ assertEquals(0, count("t_tx_sp_b"), "保存点之后的数据应被回滚");
102+ cleanup("t_tx_sp_");
103+ }
104+ 
105+ @Test
106+ @DisplayName("4.5 隔离级别接受性:READ_COMMITTED / REPEATABLE_READ / SERIALIZABLE")
107+ void isolationLevels() throws SQLException {
108+ int[] levels = {
109+ Connection.TRANSACTION_READ_COMMITTED,
110+ Connection.TRANSACTION_REPEATABLE_READ,
111+ Connection.TRANSACTION_SERIALIZABLE
112+ };
113+ String[] names = {"READ_COMMITTED", "REPEATABLE_READ", "SERIALIZABLE"};
114+ for (int i = 0; i < levels.length; i++) {
115+ try (Connection c = raw(null)) {
116+ c.setTransactionIsolation(levels[i]);
117+ log("4.5 setTransactionIsolation(%s) 被接受,当前隔离级别 = %d", names[i], c.getTransactionIsolation());
118+ } catch (SQLException e) {
119+ log("4.5 setTransactionIsolation(%s) 报错: %s", names[i], e.getMessage());
120+ }
121+ }
122+ }
123+}
@@ -0,0 +1,373 @@
1+package org.opengauss.cases;
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.math.BigDecimal;
8+import java.sql.Connection;
9+import java.sql.Date;
10+import java.sql.PreparedStatement;
11+import java.sql.ResultSet;
12+import java.sql.SQLException;
13+import java.sql.Statement;
14+import java.sql.Timestamp;
15+ 
16+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
17+import static org.junit.jupiter.api.Assertions.assertEquals;
18+import static org.junit.jupiter.api.Assertions.assertNull;
19+import static org.junit.jupiter.api.Assertions.assertTrue;
20+import static org.opengauss.cases.DbUtil.*;
21+ 
22+/**
23+ * 维度二:类型映射 + 官方协议兼容文档(dolphin_mysql_protocol_compatibility)明示的 6 个 JDBC 参数兼容点。
24+ * tinyInt1isBit / transformedBitIsBoolean / zeroDateTimeBehavior / useOldAliasMetadataBehavior
25+ * / useaffectedrows / allowMultiQueries(文档标注暂不支持的参数实测行为并记录)。
26+ */
27+class TypeMappingTest {
28+ 
29+ @BeforeAll
30+ static void createTable() throws SQLException {
31+ try (Connection c = raw(null)) {
32+ exec(c,
33+ "DROP TABLE IF EXISTS t_type_map",
34+ "CREATE TABLE t_type_map ("
35+ + " id INT AUTO_INCREMENT PRIMARY KEY,"
36+ + " t_tinyint TINYINT,"
37+ + " t_tinyint1 TINYINT(1),"
38+ + " t_bit BIT(1),"
39+ + " t_decimal DECIMAL(10,2),"
40+ + " t_date DATE,"
41+ + " t_datetime DATETIME,"
42+ + " t_timestamp TIMESTAMP,"
43+ + " t_varchar VARCHAR(50),"
44+ + " t_blob BLOB"
45+ + ")");
46+ log("2.0 建表成功:t_type_map(含 TINYINT/TINYINT(1)/BIT(1)/DECIMAL/DATE/DATETIME/TIMESTAMP/VARCHAR/BLOB)");
47+ }
48+ }
49+ 
50+ private static void insertBaseRow(Connection c) throws SQLException {
51+ try (PreparedStatement ps = c.prepareStatement(
52+ "INSERT INTO t_type_map (t_tinyint, t_tinyint1, t_bit, t_decimal,"
53+ + " t_date, t_datetime, t_timestamp, t_varchar, t_blob)"
54+ + " VALUES (?,?,?,?,?,?,?,?,?)")) {
55+ ps.setInt(1, 127);
56+ ps.setInt(2, 1);
57+ ps.setInt(3, 1);
58+ ps.setBigDecimal(4, new BigDecimal("3.14"));
59+ ps.setDate(5, Date.valueOf("2026-08-28"));
60+ ps.setTimestamp(6, Timestamp.valueOf("2026-08-28 12:34:56"));
61+ ps.setTimestamp(7, Timestamp.valueOf("2026-08-28 12:34:56"));
62+ ps.setString(8, "hello");
63+ ps.setBytes(9, new byte[]{1, 2, 3});
64+ ps.executeUpdate();
65+ }
66+ }
67+ 
68+ @Test
69+ @DisplayName("2.1 默认参数下各类型的 Java 映射")
70+ void defaultTypeMapping() throws SQLException {
71+ try (Connection c = raw(null)) {
72+ insertBaseRow(c);
73+ try (PreparedStatement ps = c.prepareStatement(
74+ "SELECT t_tinyint, t_tinyint1, t_bit, t_decimal, t_date,"
75+ + " t_datetime, t_timestamp, t_varchar, t_blob FROM t_type_map"
76+ + " WHERE t_varchar = 'hello'");
77+ ResultSet rs = ps.executeQuery()) {
78+ rs.next();
79+ log("2.1 TINYINT -> %s(值 %s)", rs.getObject(1).getClass().getSimpleName(), rs.getObject(1));
80+ log("2.1 TINYINT(1) -> %s(值 %s)", rs.getObject(2).getClass().getSimpleName(), rs.getObject(2));
81+ log("2.1 BIT(1) -> %s(值 %s)", rs.getObject(3).getClass().getSimpleName(), rs.getObject(3));
82+ log("2.1 DECIMAL -> %s(值 %s)", rs.getObject(4).getClass().getSimpleName(), rs.getObject(4));
83+ log("2.1 DATE -> %s(值 %s)", rs.getObject(5).getClass().getSimpleName(), rs.getObject(5));
84+ log("2.1 DATETIME -> %s(值 %s)", rs.getObject(6).getClass().getSimpleName(), rs.getObject(6));
85+ log("2.1 TIMESTAMP -> %s(值 %s)", rs.getObject(7).getClass().getSimpleName(), rs.getObject(7));
86+ log("2.1 VARCHAR -> %s(值 %s)", rs.getObject(8).getClass().getSimpleName(), rs.getObject(8));
87+ log("2.1 BLOB -> %s(长度 %d)", rs.getObject(9).getClass().getSimpleName(),
88+ rs.getBytes(9).length);
89+ log("2.1 服务端列类型元数据: t_tinyint1 typeName=%s jdbcType=%d, t_bit typeName=%s jdbcType=%d",
90+ rs.getMetaData().getColumnTypeName(2), rs.getMetaData().getColumnType(2),
91+ rs.getMetaData().getColumnTypeName(3), rs.getMetaData().getColumnType(3));
92+ 
93+ // 驱动默认行为断言(MySQL 驱动 8.x 的文档化默认值)
94+ assertEquals(Integer.class, rs.getObject(1).getClass(), "TINYINT 应映射 Integer");
95+ assertEquals(Boolean.class, rs.getObject(2).getClass(), "TINYINT(1) 默认 tinyInt1isBit=true 应映射 Boolean");
96+ // 官方协议兼容文档:transformedBitIsBoolean 默认值为 false,但备注"不管是否转换,在 Java 中都是 Boolean 类型"
97+ assertEquals(Boolean.class, rs.getObject(3).getClass(), "BIT(1) 默认参数下映射 Boolean(与官方文档备注一致)");
98+ assertEquals(BigDecimal.class, rs.getObject(4).getClass(), "DECIMAL 应映射 BigDecimal");
99+ assertEquals(Date.class, rs.getObject(5).getClass(), "DATE 应映射 java.sql.Date");
100+ // MySQL 8.x 驱动文档行为:getObject 对 DATETIME 返回 LocalDateTime(不是 Timestamp)
101+ assertEquals(java.time.LocalDateTime.class, rs.getObject(6).getClass(),
102+ "DATETIME 应映射 LocalDateTime(驱动 8.x 文档行为)");
103+ assertEquals(Timestamp.class, rs.getObject(7).getClass(), "TIMESTAMP 应映射 Timestamp");
104+ assertEquals(String.class, rs.getObject(8).getClass(), "VARCHAR 应映射 String");
105+ }
106+ }
107+ }
108+ 
109+ @Test
110+ @DisplayName("2.2 tinyInt1isBit=false:TINYINT(1) 应映射 Integer")
111+ void tinyInt1isBitFalse() throws SQLException {
112+ try (Connection c = raw("tinyInt1isBit=false")) {
113+ insertBaseRow(c);
114+ try (PreparedStatement ps = c.prepareStatement(
115+ "SELECT t_tinyint1 FROM t_type_map WHERE t_varchar = 'hello'");
116+ ResultSet rs = ps.executeQuery()) {
117+ rs.next();
118+ Object v = rs.getObject(1);
119+ log("2.2 tinyInt1isBit=false: TINYINT(1) -> %s(值 %s)", v.getClass().getSimpleName(), v);
120+ assertEquals(Integer.class, v.getClass());
121+ assertEquals(1, v);
122+ }
123+ }
124+ }
125+ 
126+ @Test
127+ @DisplayName("2.3 transformedBitIsBoolean=true/false:BIT(1) 的 getObject 映射(官方文档备注:Java 侧均为 Boolean)")
128+ void transformedBitIsBoolean() throws SQLException {
129+ insertBaseRow(raw(null));
130+ for (String param : new String[]{"transformedBitIsBoolean=false", "transformedBitIsBoolean=true"}) {
131+ try (Connection c = raw(param)) {
132+ try (PreparedStatement ps = c.prepareStatement(
133+ "SELECT t_bit FROM t_type_map WHERE t_varchar = 'hello'");
134+ ResultSet rs = ps.executeQuery()) {
135+ rs.next();
136+ Object v = rs.getObject(1);
137+ byte[] b = rs.getBytes(1);
138+ log("2.3 %s: BIT(1) getObject -> %s(值 %s),getBytes -> byte[%d]",
139+ param, v.getClass().getSimpleName(), v, b.length);
140+ // 官方文档备注:"不管是否转换,在 Java 中都是 Boolean 类型" —— 实测两种设置均映射 Boolean
141+ assertEquals(Boolean.class, v.getClass(),
142+ param + " 下 BIT(1) getObject 应恒为 Boolean(官方文档备注)");
143+ // getBytes 对 BIT 列恒返回原始字节,与参数无关
144+ assertArrayEquals(new byte[]{1}, b);
145+ }
146+ }
147+ }
148+ }
149+ 
150+ @Test
151+ @DisplayName("2.4 BLOB 读写往返")
152+ void blobRoundtrip() throws SQLException {
153+ byte[] data = new byte[]{9, 8, 7, 6, 5, 4, 3, 2, 1};
154+ try (Connection c = raw(null)) {
155+ try (PreparedStatement ps = c.prepareStatement(
156+ "INSERT INTO t_type_map (t_blob) VALUES (?)")) {
157+ ps.setBytes(1, data);
158+ ps.executeUpdate();
159+ }
160+ try (PreparedStatement ps = c.prepareStatement(
161+ "SELECT t_blob FROM t_type_map ORDER BY id DESC LIMIT 1");
162+ ResultSet rs = ps.executeQuery()) {
163+ rs.next();
164+ byte[] back = rs.getBytes(1);
165+ log("2.4 BLOB 往返: 写入 %d 字节,读回 %d 字节", data.length, back.length);
166+ assertArrayEquals(data, back);
167+ }
168+ }
169+ }
170+ 
171+ @Test
172+ @DisplayName("2.5 zeroDateTimeBehavior:零日期由 dolphin.sql_mode 的 no_zero_date 控制(默认拒绝、去掉可存储,三档读取与 MySQL 一致)")
173+ void zeroDateTime() throws SQLException {
174+ // ① 默认配置(dolphin.sql_mode 含 no_zero_date):写入零日期被拒
175+ try (Connection c = raw(null)) {
176+ try (PreparedStatement ps = c.prepareStatement(
177+ "INSERT INTO t_type_map (t_datetime) VALUES (?)")) {
178+ ps.setString(1, "0000-00-00 00:00:00");
179+ ps.executeUpdate();
180+ log("2.5 注意:默认 sql_mode 下零日期写入成功,说明测试库未开启 no_zero_date(与文档默认口径不同)");
181+ } catch (SQLException e) {
182+ assertTrue(e.getMessage() != null && e.getMessage().contains("out of range"),
183+ "默认 sql_mode(含 no_zero_date)应拒绝零日期,实际: " + e.getMessage());
184+ log("2.5 默认 sql_mode(含 no_zero_date)拒绝零日期写入: %s", e.getMessage());
185+ }
186+ }
187+ // ② 去掉 no_zero_date(会话级 SET):零日期可正常存储
188+ String sqlMode = null;
189+ try (Connection c = raw(null);
190+ Statement st = c.createStatement()) {
191+ try (ResultSet rs = st.executeQuery("SELECT @@sql_mode")) {
192+ if (rs.next()) {
193+ sqlMode = rs.getString(1);
194+ }
195+ } catch (SQLException e) {
196+ log("2.5 @@sql_mode 读取失败(%s),改走 current_setting", e.getMessage());
197+ try (ResultSet rs = st.executeQuery("SELECT current_setting('dolphin.sql_mode')")) {
198+ if (rs.next()) {
199+ sqlMode = rs.getString(1);
200+ }
201+ }
202+ }
203+ } catch (SQLException e) {
204+ log("2.5 读取 sql_mode 失败(%s),跳过零日期存储/读取验证", e.getMessage());
205+ }
206+ if (sqlMode != null && sqlMode.contains("no_zero_date")) {
207+ // 诊断结论:dolphin.sql_mode 的 GUC 校验要求——值里含 STRICT_TRANS_TABLES
208+ // (MySQL 协议层附加 token)时必须是全量列表,否则报 invalid value。
209+ // 故去掉 no_zero_date 的同时须一并去掉 STRICT_TRANS_TABLES(实测 SET 后零日期
210+ // 可写入;SET SESSION sql_mode=... 的 MySQL 写法会被协议层拦截、实际不生效,
211+ // 必须用 SET dolphin.sql_mode=...)。
212+ String relaxed = sqlMode.replace("no_zero_date", "")
213+ .replace("STRICT_TRANS_TABLES", "")
214+ .replaceAll(",\\s*,", ",")
215+ .replaceAll("^\\s*,|,\\s*$", "");
216+ if (relaxed.trim().isEmpty()) {
217+ log("2.5 sql_mode 仅含 no_zero_date,跳过零日期存储/读取验证");
218+ return;
219+ }
220+ int zeroId;
221+ try (Connection c = raw(null)) {
222+ try (Statement st = c.createStatement()) {
223+ st.execute("SET dolphin.sql_mode = '" + relaxed + "'");
224+ }
225+ try (PreparedStatement ps = c.prepareStatement(
226+ "INSERT INTO t_type_map (t_datetime) VALUES (?)")) {
227+ ps.setString(1, "0000-00-00 00:00:00");
228+ assertEquals(1, ps.executeUpdate(), "去掉 no_zero_date 后零日期应可写入");
229+ }
230+ try (Statement st = c.createStatement();
231+ ResultSet rs = st.executeQuery("SELECT MAX(id) FROM t_type_map")) {
232+ rs.next();
233+ zeroId = rs.getInt(1);
234+ }
235+ log("2.5 去掉 no_zero_date 后零日期写入成功(id=%d)", zeroId);
236+ }
237+ // ③ 表中有零日期时 zeroDateTimeBehavior 三档读取行为(与 MySQL 语义一致)
238+ for (String mode : new String[]{"CONVERT_TO_NULL", "EXCEPTION", "ROUND"}) {
239+ try (Connection c = raw("zeroDateTimeBehavior=" + mode);
240+ PreparedStatement ps = c.prepareStatement(
241+ "SELECT t_datetime FROM t_type_map WHERE id = ?")) {
242+ ps.setInt(1, zeroId);
243+ try (ResultSet rs = ps.executeQuery()) {
244+ rs.next();
245+ Object v = rs.getObject(1);
246+ log("2.5 zeroDateTimeBehavior=%s: 读到 %s(%s)",
247+ mode, String.valueOf(v), v == null ? "null" : v.getClass().getSimpleName());
248+ if ("CONVERT_TO_NULL".equals(mode)) {
249+ assertNull(v, "convertToNull 档应读为 null(与 MySQL 语义一致)");
250+ } else if ("ROUND".equals(mode)) {
251+ assertTrue(String.valueOf(v).startsWith("0001-01-01"),
252+ "round 档应读为 0001-01-01,实际: " + v);
253+ }
254+ }
255+ } catch (SQLException e) {
256+ if ("EXCEPTION".equals(mode)) {
257+ assertTrue(e.getMessage() != null
258+ && e.getMessage().contains("Zero date value prohibited"),
259+ "exception 档应抛 Zero date value prohibited,实际: " + e.getMessage());
260+ log("2.5 zeroDateTimeBehavior=%s: 读取按预期抛异常: %s", mode, e.getMessage());
261+ } else {
262+ throw e;
263+ }
264+ }
265+ }
266+ }
267+ }
268+ 
269+ @Test
270+ @DisplayName("2.6 useOldAliasMetadataBehavior:表达式别名 vs 表列别名(官方文档备注:表达式别名始终返回别名)")
271+ void useOldAliasMetadataBehavior() throws SQLException {
272+ String exprNameTrue = null, exprNameDefault = null;
273+ String colNameTrue = null, colNameDefault = null;
274+ // 场景一:表达式别名(SELECT 1 AS my_alias)——文档备注:始终返回别名,与参数无关
275+ for (String param : new String[]{"useOldAliasMetadataBehavior=true", null}) {
276+ try (Connection c = raw(param)) {
277+ try (PreparedStatement ps = c.prepareStatement("SELECT 1 AS my_alias");
278+ ResultSet rs = ps.executeQuery()) {
279+ var md = rs.getMetaData();
280+ log("2.6 表达式别名(%s): columnName=%s, columnLabel=%s",
281+ param == null ? "默认" : param, md.getColumnName(1), md.getColumnLabel(1));
282+ if (param == null) {
283+ exprNameDefault = md.getColumnName(1);
284+ } else {
285+ exprNameTrue = md.getColumnName(1);
286+ }
287+ }
288+ }
289+ }
290+ // 场景二:表列别名(SELECT name AS n)——参数真正的生效场景
291+ for (String param : new String[]{"useOldAliasMetadataBehavior=true", null}) {
292+ try (Connection c = raw(param)) {
293+ try (PreparedStatement ps = c.prepareStatement("SELECT name AS n FROM `user` LIMIT 1");
294+ ResultSet rs = ps.executeQuery()) {
295+ var md = rs.getMetaData();
296+ log("2.6 表列别名(%s): columnName=%s, columnLabel=%s",
297+ param == null ? "默认" : param, md.getColumnName(1), md.getColumnLabel(1));
298+ if (param == null) {
299+ colNameDefault = md.getColumnName(1);
300+ } else {
301+ colNameTrue = md.getColumnName(1);
302+ }
303+ }
304+ }
305+ }
306+ // 断言:表达式别名始终返回别名(文档备注);表列别名 true 时 getColumnName 返回别名,false 时返回原始列名(文档:目前仅支持 getColumnName)
307+ assertEquals("my_alias", exprNameTrue, "表达式别名 + true 应返回别名");
308+ assertEquals("my_alias", exprNameDefault, "表达式别名 + 默认应返回别名(文档备注)");
309+ assertEquals("n", colNameTrue, "表列别名 + true 时 getColumnName 应返回别名 n(参数生效)");
310+ assertEquals("name", colNameDefault, "表列别名 + 默认时 getColumnName 应返回原始列名 name");
311+ }
312+ 
313+ @Test
314+ @DisplayName("2.7 useaffectedrows:无变化 UPDATE 的影响行数(官方文档备注:目前不支持,始终返回找到的行数)")
315+ void useAffectedRows() throws SQLException {
316+ try (Connection c0 = raw(null)) {
317+ try (PreparedStatement ps = c0.prepareStatement(
318+ "INSERT INTO t_type_map (t_varchar, t_tinyint) VALUES ('n7_hello', 5)")) {
319+ ps.executeUpdate();
320+ }
321+ }
322+ for (String param : new String[]{"useaffectedrows=true", null}) {
323+ try (Connection c = raw(param)) {
324+ int updated;
325+ try (PreparedStatement ps = c.prepareStatement(
326+ "UPDATE t_type_map SET t_tinyint = t_tinyint WHERE t_varchar = 'n7_hello'")) {
327+ updated = ps.executeUpdate();
328+ }
329+ log("2.7 %s: 无变化 UPDATE 影响行数 = %d(MySQL 原生差异:默认=找到行 1,true=受影响行 0)",
330+ param == null ? "默认" : param, updated);
331+ // 官方文档备注:"目前不支持该参数……openGauss 始终返回'找到的行'的个数" —— 实测两种设置均返回 1
332+ assertEquals(1, updated, "useaffectedrows 下无变化 UPDATE 应始终返回找到的行数 1(官方文档备注)");
333+ }
334+ }
335+ }
336+ 
337+ @Test
338+ @DisplayName("2.8 allowMultiQueries:多语句执行(官方文档:默认 false,true 时允许 ';' 分隔多查询)")
339+ void allowMultiQueries() throws SQLException {
340+ int resultsTrue = -1;
341+ try (Connection c = raw("allowMultiQueries=true")) {
342+ try (Statement st = c.createStatement()) {
343+ boolean hasResults = st.execute("SELECT 1 AS a; SELECT 2 AS b");
344+ int count = 0;
345+ do {
346+ count++;
347+ if (hasResults) {
348+ try (ResultSet rs = st.getResultSet()) {
349+ rs.next();
350+ log("2.8 allowMultiQueries=true: 第 %d 个结果集第一列值 = %s", count, rs.getObject(1));
351+ }
352+ } else {
353+ log("2.8 allowMultiQueries=true: 第 %d 个结果为更新计数 %d", count, st.getUpdateCount());
354+ }
355+ hasResults = st.getMoreResults();
356+ } while (hasResults || st.getUpdateCount() != -1);
357+ resultsTrue = count;
358+ log("2.8 allowMultiQueries=true: 共收到 %d 个结果", count);
359+ }
360+ }
361+ boolean defaultThrew = false;
362+ try (Connection c = raw(null)) {
363+ try (Statement st = c.createStatement()) {
364+ st.execute("SELECT 1 AS a; SELECT 2 AS b");
365+ } catch (SQLException e) {
366+ defaultThrew = true;
367+ log("2.8 默认: 多语句执行报错: SQLState=%s, msg=%s", e.getSQLState(), e.getMessage());
368+ }
369+ }
370+ assertEquals(2, resultsTrue, "allowMultiQueries=true 应返回 2 个结果集");
371+ assertTrue(defaultThrew, "默认配置下多语句应被服务端拒绝");
372+ }
373+}
@@ -0,0 +1,57 @@
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.jdbc.core.JdbcTemplate;
7+ 
8+import java.util.List;
9+ 
10+import static org.junit.jupiter.api.Assertions.assertEquals;
11+ 
12+/**
13+ * 冒烟用例:需真实 openGauss B 库环境(README 前置条件满足后才能运行)。
14+ * 同一套用例配合 pom 里不同 driver profile 执行,即构成驱动版本扫测矩阵的用例组。
15+ */
16+@SpringBootTest
17+class SmokeTest {
18+ 
19+ @Autowired
20+ private JdbcTemplate jdbc;
21+ 
22+ @Autowired
23+ private TxService txService;
24+ 
25+ @Test
26+ void crud() {
27+ jdbc.update("INSERT INTO `user` (`name`, `age`) VALUES (?, ?)", "t_crud", 1);
28+ Integer age = jdbc.queryForObject(
29+ "SELECT `age` FROM `user` WHERE `name` = ?", Integer.class, "t_crud");
30+ assertEquals(1, age);
31+ jdbc.update("UPDATE `user` SET `age` = ? WHERE `name` = ?", 2, "t_crud");
32+ jdbc.update("DELETE FROM `user` WHERE `name` = ?", "t_crud");
33+ }
34+ 
35+ @Test
36+ void batch() {
37+ List<Object[]> args = List.of(
38+ new Object[]{"t_batch1", 10},
39+ new Object[]{"t_batch2", 11});
40+ int[] rows = jdbc.batchUpdate(
41+ "INSERT INTO `user` (`name`, `age`) VALUES (?, ?)", args);
42+ assertEquals(2, rows.length);
43+ jdbc.update("DELETE FROM `user` WHERE `name` LIKE 't_batch%'");
44+ }
45+ 
46+ @Test
47+ void transactionRollback() {
48+ try {
49+ txService.insertThenThrow("t_tx", 1);
50+ } catch (RuntimeException expected) {
51+ // 预期异常:触发回滚
52+ }
53+ Integer count = jdbc.queryForObject(
54+ "SELECT COUNT(*) FROM `user` WHERE `name` = 't_tx'", Integer.class);
55+ assertEquals(0, count, "事务应已回滚,表中不应存在 t_tx");
56+ }
57+}