已开启
【实习#27】新增 Spring Boot 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #110
wsrym创建于 5 天前
【实习#27】新增 Spring Boot 配合 MySQL JDBC 连接 openGauss B 库示例与自测 #110
已开启
共 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 | + | ||
| 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 | + | ||
| 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 | + | ||
| 14 | +public class SmokeApplication { | ||
| 15 | + | ||
| 16 | + public static void main(String[] args) { | ||
| 17 | + SpringApplication.run(SmokeApplication.class, args); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + | ||
| 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 | + | ||
| 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 | + | ||
| 9 | +public class TxService { | ||
| 10 | + | ||
| 11 | + private final JdbcTemplate jdbc; | ||
| 12 | + | ||
| 13 | + public TxService(JdbcTemplate jdbc) { | ||
| 14 | + this.jdbc = jdbc; | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + | ||
| 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 | + | ||
| 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 | + | ||
| 31 | + | ||
| 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 | + | ||
| 53 | + | ||
| 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 | + | ||
| 75 | + | ||
| 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 | + | ||
| 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 | + | ||
| 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 | + | ||
| 24 | +class ConnectionAuthTest { | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + private DataSource dataSource; | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + | ||
| 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 | + | ||
| 50 | + | ||
| 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 | + | ||
| 58 | + | ||
| 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 | + | ||
| 66 | + | ||
| 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 | + | ||
| 27 | +class ConnectionPoolTest { | ||
| 28 | + | ||
| 29 | + | ||
| 30 | + private DataSource dataSource; | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + | ||
| 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 | + | ||
| 43 | + | ||
| 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 | + | ||
| 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 | + | ||
| 46 | + | ||
| 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 | + | ||
| 70 | + | ||
| 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 | + | ||
| 88 | + | ||
| 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 | + | ||
| 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 | + | ||
| 65 | + | ||
| 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 | + | ||
| 80 | + | ||
| 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 | + | ||
| 93 | + | ||
| 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 | + | ||
| 103 | + | ||
| 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 | + | ||
| 125 | + | ||
| 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 | + | ||
| 183 | + | ||
| 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 | + | ||
| 202 | + | ||
| 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 | + | ||
| 223 | + | ||
| 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 | + | ||
| 257 | + | ||
| 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 | + | ||
| 24 | +class TransactionTest { | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + private JdbcTemplate jdbc; | ||
| 28 | + | ||
| 29 | + | ||
| 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 | + | ||
| 43 | + | ||
| 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 | + | ||
| 54 | + | ||
| 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 | + | ||
| 66 | + | ||
| 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 | + | ||
| 86 | + | ||
| 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 | + | ||
| 106 | + | ||
| 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 | + | ||
| 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 | + | ||
| 69 | + | ||
| 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 | + | ||
| 110 | + | ||
| 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 | + | ||
| 127 | + | ||
| 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 | + | ||
| 151 | + | ||
| 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 | + | ||
| 172 | + | ||
| 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 | + | ||
| 270 | + | ||
| 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 | + | ||
| 314 | + | ||
| 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 | + | ||
| 338 | + | ||
| 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 | + | ||
| 17 | +class SmokeTest { | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + private JdbcTemplate jdbc; | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + private TxService txService; | ||
| 24 | + | ||
| 25 | + | ||
| 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 | + | ||
| 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 | + | ||
| 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 | +} | ||