已开启
add: DBCP 连接池适配 openGauss B 兼容模式示例与测试 #104
add: DBCP 连接池适配 openGauss B 兼容模式示例与测试 #104
已开启
liuhongwei创建于 8月7日
26 个文件变更+3255-0
@@ -0,0 +1,205 @@
1+# DBCP 连接 openGauss B 兼容模式示例
2+ 
3+本项目演示 Apache Commons DBCP2 通过 MySQL Connector/J 和 Dolphin MySQL 协议端口
4+访问 openGauss B 兼容模式数据库,并提供连接池、CRUD、事务、并发和 SQL 分类测试。
5+ 
6+## 环境要求
7+ 
8+| 组件 | 版本或要求 |
9+|---|---|
10+| JDK | 8+ |
11+| Maven | 3.6+ |
12+| Apache Commons DBCP2 | 2.9.0 |
13+| Apache Commons Pool2 | 2.10.0(DBCP2 传递依赖) |
14+| MySQL Connector/J | 8.0.28 |
15+| openGauss | 包含 Dolphin 插件并启用 MySQL 协议 |
16+ 
17+DBCP2 2.9.0 和 Connector/J 8.0.28 的核心类使用 Java 8 字节码,因此最低运行版本为
18+JDK 8。openGauss 服务端与 Dolphin 应使用同一官方安装包,无需单独编译 Dolphin。
19+ 
20+本文命令默认使用 Dolphin 端口 `3308`。自测报告使用独立实例端口 `36443`,通过
21+`-Ddbcp.url` 覆盖连接地址;端口不同只代表实例隔离,连接链路和测试内容相同。
22+ 
23+## 连接链路
24+ 
25+```text
26+dbcp.properties
27+ -> BasicDataSourceFactory
28+ -> BasicDataSource.getConnection()
29+ -> GenericObjectPool.borrowObject()
30+ -> PoolableConnectionFactory.makeObject()(池中无空闲连接时)
31+ -> DriverConnectionFactory.createConnection()
32+ -> com.mysql.cj.jdbc.Driver.connect()
33+ -> com.mysql.cj.jdbc.ConnectionImpl
34+ -> TCP / Dolphin MySQL 协议端口
35+ -> openGauss B 兼容库
36+ -> ResultSet 或 updateCount
37+ -> PoolableConnection.close()
38+ -> GenericObjectPool.returnObject() 将连接归还 DBCP
39+```
40+ 
41+`BasicDataSource` 管理池参数和连接生命周期。仅当池内没有可复用的空闲连接且未达到
42+`maxTotal` 时,DBCP 才调用 Connector/J 创建物理连接;Connector/J 负责 MySQL 协议通信,
43+Dolphin 将协议请求交给 openGauss 执行。业务代码调用 `Connection.close()` 时,DBCP
44+归还逻辑连接;物理连接是否保留由连接池配置决定。
45+ 
46+## Spring Boot 快速开始
47+ 
48+工程同时提供一个可直接运行的 Spring Boot 入口,用于验证真实的应用链路:
49+ 
50+```text
51+SpringApplication.run
52+ -> Spring Boot DataSourceAutoConfiguration
53+ -> BasicDataSource(DBCP2)
54+ -> JdbcTemplate / TransactionTemplate
55+ -> MySQL Connector/J
56+ -> Dolphin MySQL 协议
57+ -> openGauss B 兼容库
58+```
59+ 
60+`spring-boot-starter-jdbc` 默认会选择 HikariCP。本工程在 `pom.xml` 排除了 HikariCP,
61+并在 `application.properties` 显式指定 `BasicDataSource`,因此运行时不会误用其他连接池。
62+完成数据库初始化后执行:
63+ 
64+```bash
65+export DBCP_PASSWORD='实际密码'
66+mvn spring-boot:run -Dspring-boot.run.jvmArguments="-DDBCP_PASSWORD=$DBCP_PASSWORD"
67+```
68+ 
69+也可以通过环境变量覆盖地址和用户:
70+ 
71+```bash
72+DBCP_URL='jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC' \
73+DBCP_USER=opengauss DBCP_PASSWORD='实际密码' mvn spring-boot:run
74+```
75+ 
76+启动日志应包含 `BasicDataSource`、连接池参数、`JdbcTemplate` 查询行数以及
77+`TransactionTemplate COMMIT/ROLLBACK` 结果。示例会删除自己插入的临时数据。
78+在 VM master 转测包实例(Dolphin 36443)上已验证通过;使用专用账号时请确保同时授予
79+schema、表和自增序列权限。
80+ 
81+## 普通 JDBC 示例与测试
82+ 
83+### 1. 配置 Dolphin
84+ 
85+`postgresql.conf` 中设置:
86+ 
87+```ini
88+shared_preload_libraries = 'dolphin'
89+enable_dolphin_proto = on
90+dolphin_server_port = 3308
91+dolphin.default_database_name = 'proto_test_db'
92+```
93+ 
94+重启实例并确认端口监听:
95+ 
96+```bash
97+gs_ctl restart -D "$PGDATA" -Z single_node -l "$GAUSSLOG/restart.log"
98+ss -tlnp | grep 3308
99+```
100+ 
101+### 2. 配置客户端认证
102+ 
103+以下配置仅允许本机访问。远程连接时应改为实际客户端所在的受限网段。
104+ 
105+```ini
106+local all all trust
107+host all all 127.0.0.1/32 sha256
108+host all all ::1/128 sha256
109+```
110+ 
111+修改 `pg_hba.conf` 后执行:
112+ 
113+```bash
114+gs_ctl reload -D "$PGDATA"
115+```
116+ 
117+### 3. 初始化数据库
118+ 
119+先将 `src/main/resources/create_b_database.sql` 中的 `<DBCP_PASSWORD>` 替换为测试密码,
120+再按以下顺序执行脚本:
121+ 
122+```bash
123+gsql -d postgres -h "$PGDATA" -p 5432 \
124+ -f src/main/resources/create_b_database.sql
125+gsql -d proto_test_db -h "$PGDATA" -p 5432 \
126+ -f src/main/resources/init.sql
127+```
128+ 
129+脚本创建 B 兼容库 `proto_test_db`、schema `mysql_test_db``user` 表和初始数据。
130+ 
131+### 4. 配置连接池
132+ 
133+编辑 `src/main/resources/dbcp.properties`
134+ 
135+```properties
136+driverClassName=com.mysql.cj.jdbc.Driver
137+url=jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL
138+username=opengauss
139+password=<DBCP_PASSWORD>
140+ 
141+initialSize=2
142+maxTotal=8
143+maxIdle=8
144+minIdle=2
145+maxWaitMillis=5000
146+ 
147+validationQuery=SELECT 1
148+testOnBorrow=true
149+testWhileIdle=true
150+```
151+ 
152+### 5. 运行测试和示例
153+ 
154+测试密码也可以通过系统属性传入,避免修改配置文件:
155+ 
156+```bash
157+mvn clean test -Ddbcp.password="$DBCP_PASSWORD"
158+mvn exec:java -Dexec.mainClass=org.opengauss.DbcpConnectDemo
159+```
160+ 
161+全量测试的预期结果为:
162+ 
163+```text
164+Tests run: 46, Failures: 0, Errors: 0, Skipped: 0
165+BUILD SUCCESS
166+```
167+ 
168+## 连接池边界
169+ 
170+`maxTotal=8` 表示单个数据源最多同时借出 8 条连接。连接池满后,新请求最多等待
171+`maxWaitMillis`;等待期间不得突破 `maxTotal`。连接归还后,排队请求应继续执行;等待
172+超时则抛出 `SQLException`
173+ 
174+`ConnectionPoolTest#testMaxTotalConcurrentCrudAndQueuedBorrow` 使用 8 条连接分别执行
175+`INSERT -> SELECT -> UPDATE -> SELECT -> DELETE -> COMMIT` 并保持占用,同时验证第 9 条
176+请求等待、超时和释放后的恢复行为。
177+ 
178+`StabilityTest#testConcurrentCrudLoad` 使用 16 个并发连接执行完整事务 CRUD,并在结束时
179+断言活动连接数归零。更高负载测试固定 `maxTotal=128`,验证了 128 条同时持有连接和最多
180+5000 个排队业务请求;请求数不等于物理连接数。
181+ 
182+## 测试范围
183+ 
184+| 测试类 | 用例数 | 主要内容 |
185+|---|---:|---|
186+| `DbcpDataSourceTest` | 5 | 配置装配、驱动加载、初始连接及错误连接参数 |
187+| `ConnectionPoolTest` | 10 | 借出归还、池参数、并发、满池等待与恢复 |
188+| `ConnectionValidationTest` | 3 | `testOnBorrow``testWhileIdle` 和断连恢复 |
189+| `CrudTest` | 8 | 查询、插入、更新、删除、分页和批处理 |
190+| `TransactionTest` | 5 | 自动提交、提交、回滚、隔离级别和池化连接事务 |
191+| `SqlCategoryTest` | 5 | DDL、DML、DQL、DCL、TCL |
192+| `DataTypeCharsetTest` | 3 | 中文、emoji、常用类型和 NULL |
193+| `ConstraintTest` | 3 | 多语句限制、游标参数和非 SSL 连接 |
194+| `StabilityTest` | 4 | 重复建池、并发事务、长连接和池状态 |
195+| **合计** | **46** | |
196+ 
197+DDL、DML、DQL、DCL、TCL 用例分别校验对象状态、影响行数、查询结果、授权状态以及
198+事务提交和回滚后的数据可见性。examples 的 DCL 测试使用具备 `CREATEROLE` 权限的
199+`opengauss` 测试账号;生产部署应改用专用业务账号,并按实际权限调整 DCL 用例。
200+ 
201+## 相关文档
202+ 
203+- `docs/初始化步骤.md`:完整初始化命令。
204+- `docs/测试思维导图.md`:测试范围思维导图。
205+- `docs/自测报告.md`:测试环境、结果和用例映射。
@@ -0,0 +1,55 @@
1+# Spring Boot + DBCP 连接 openGauss B 兼容模式快速开始
2+ 
3+## 环境要求
4+ 
5+- JDK 8+、Maven 3.6+
6+- Apache Commons DBCP2 2.9.0、Commons Pool2 2.10.0
7+- MySQL Connector/J 8.0.28
8+- 包含 Dolphin 插件并已启用 MySQL 协议的 openGauss
9+ 
10+## 操作步骤
11+ 
12+1.`postgresql.conf` 中配置并重启实例:
13+ 
14+ ```ini
15+ shared_preload_libraries = 'dolphin'
16+ enable_dolphin_proto = on
17+ dolphin_server_port = 3308
18+ dolphin.default_database_name = 'proto_test_db'
19+ ```
20+ 
21+2. 确认 Dolphin 端口监听:
22+ 
23+ ```bash
24+ ss -tlnp | grep 3308
25+ ```
26+ 
27+3.`src/main/resources/create_b_database.sql` 中的 `<DBCP_PASSWORD>` 替换为测试密码,
28+ 然后初始化数据库:
29+ 
30+ ```bash
31+ gsql -d postgres -h "$PGDATA" -p 5432 \
32+ -f src/main/resources/create_b_database.sql
33+ gsql -d proto_test_db -h "$PGDATA" -p 5432 \
34+ -f src/main/resources/init.sql
35+ ```
36+ 
37+4. 运行 Spring Boot 链路示例:
38+ 
39+ ```bash
40+ DBCP_PASSWORD='实际密码' mvn spring-boot:run
41+ ```
42+ 
43+ 日志应显示实际数据源为 `org.apache.commons.dbcp2.BasicDataSource`,并显示
44+ `JdbcTemplate` 查询、事务提交和回滚结果。
45+ 
46+5. 运行 46 项低层测试和普通 JDBC 示例:
47+ 
48+ ```bash
49+ mvn clean test -Ddbcp.password="$DBCP_PASSWORD"
50+ mvn exec:java -Dexec.mainClass=org.opengauss.DbcpConnectDemo
51+ ```
52+ 
53+预期结果:`Tests run: 46, Failures: 0, Errors: 0, Skipped: 0``BUILD SUCCESS`
54+ 
55+完整说明见 `../README.md``../docs/初始化步骤.md``../docs/自测报告.md`
@@ -0,0 +1,95 @@
1+# 初始化步骤
2+ 
3+本文说明如何为 DBCP 示例准备 openGauss B 兼容数据库。openGauss 应使用包含 Dolphin
4+插件的官方安装包。
5+ 
6+## 前置条件
7+ 
8+- openGauss 实例已使用 `gs_initdb` 初始化。
9+- 当前用户可以执行 `gs_ctl``gsql`
10+- 已设置 `PGDATA``GAUSSLOG`
11+ 
12+## 1. 配置 Dolphin
13+ 
14+编辑 `$PGDATA/postgresql.conf`
15+ 
16+```ini
17+shared_preload_libraries = 'dolphin'
18+enable_dolphin_proto = on
19+dolphin_server_port = 3308
20+dolphin.default_database_name = 'proto_test_db'
21+```
22+ 
23+重启实例并确认两个协议端口均正常:
24+ 
25+```bash
26+gs_ctl restart -D "$PGDATA" -Z single_node -l "$GAUSSLOG/restart.log"
27+gs_ctl query -D "$PGDATA"
28+ss -tlnp | grep -E '5432|3308'
29+```
30+ 
31+## 2. 配置客户端认证
32+ 
33+编辑 `$PGDATA/pg_hba.conf`。以下示例只允许本机连接:
34+ 
35+```ini
36+local all all trust
37+host all all 127.0.0.1/32 sha256
38+host all all ::1/128 sha256
39+```
40+ 
41+远程连接时应将地址改为实际客户端所在的受限网段。修改后重新加载配置:
42+ 
43+```bash
44+gs_ctl reload -D "$PGDATA"
45+```
46+ 
47+## 3. 初始化数据库
48+ 
49+1.`src/main/resources/create_b_database.sql` 中的 `<DBCP_PASSWORD>` 替换为测试密码。
50+2. 使用相同密码配置 `src/main/resources/dbcp.properties`
51+3. 依次执行建库和建表脚本:
52+ 
53+```bash
54+gsql -d postgres -h "$PGDATA" -p 5432 \
55+ -f src/main/resources/create_b_database.sql
56+gsql -d proto_test_db -h "$PGDATA" -p 5432 \
57+ -f src/main/resources/init.sql
58+```
59+ 
60+第一个脚本创建 B 兼容库 `proto_test_db`、schema `mysql_test_db` 并设置 MySQL native
61+密码;第二个脚本创建 `user` 表并写入三条初始数据。
62+ 
63+如果 Spring Boot 使用非表 owner 的专用业务账号,需额外授予 schema、表和自增序列权限:
64+ 
65+```sql
66+GRANT ALL ON SCHEMA mysql_test_db TO <DBCP_USER>;
67+GRANT ALL ON ALL TABLES IN SCHEMA mysql_test_db TO <DBCP_USER>;
68+GRANT ALL ON ALL SEQUENCES IN SCHEMA mysql_test_db TO <DBCP_USER>;
69+```
70+ 
71+## 4. Spring Boot 链路验证
72+ 
73+完成初始化后,在工程目录执行:
74+ 
75+```bash
76+DBCP_PASSWORD='实际密码' mvn spring-boot:run
77+```
78+ 
79+程序会由 Spring Boot 创建 DBCP `BasicDataSource`,通过 `JdbcTemplate` 查询业务表,
80+再用 `TransactionTemplate` 分别验证提交和回滚。日志中的数据源类型必须为
81+`org.apache.commons.dbcp2.BasicDataSource`,否则说明依赖或配置未按文档设置。
82+ 
83+## 5. 低层测试和 JDBC 示例
84+ 
85+```bash
86+mvn clean test -Ddbcp.password="$DBCP_PASSWORD"
87+mvn exec:java -Dexec.mainClass=org.opengauss.DbcpConnectDemo
88+```
89+ 
90+全量测试应输出:
91+ 
92+```text
93+Tests run: 46, Failures: 0, Errors: 0, Skipped: 0
94+BUILD SUCCESS
95+```
@@ -0,0 +1,95 @@
1+# DBCP 连接 openGauss B 兼容模式测试思维导图
2+ 
3+```text
4+DBCP 连接 openGauss B 兼容模式测试
5+
6+├── 1. 环境与依赖
7+│ ├── openGauss B 兼容库
8+│ ├── Dolphin MySQL 协议端口
9+│ ├── JDK 8+ / Maven 3.6+
10+│ ├── DBCP2 2.9.0 / Pool2 2.10.0
11+│ └── MySQL Connector/J 8.0.28
12+
13+├── 2. 数据源配置
14+│ ├── dbcp.properties 参数装配
15+│ ├── 驱动类、JDBC URL、用户名与密码
16+│ ├── initialSize / maxTotal
17+│ ├── maxIdle / minIdle / maxWaitMillis
18+│ ├── validationQuery / testOnBorrow / testWhileIdle
19+│ └── 错误账号、密码和端口
20+
21+├── 3. 连接池行为
22+│ ├── 连接借出与归还
23+│ ├── 物理会话复用
24+│ ├── 池化连接状态复位
25+│ ├── 空闲连接驱逐与补足
26+│ ├── 满池等待、超时与释放后恢复
27+│ └── 失效连接淘汰与重建
28+
29+├── 4. CRUD 操作
30+│ ├── INSERT 与自增主键回填
31+│ ├── SELECT 全量、按主键、LIKE、分页
32+│ ├── UPDATE
33+│ ├── DELETE
34+│ └── JDBC batch 批量插入
35+
36+├── 5. 事务操作
37+│ ├── autoCommit 默认值
38+│ ├── COMMIT 后跨连接可见
39+│ ├── ROLLBACK 后数据不可见
40+│ ├── READ_COMMITTED / REPEATABLE_READ
41+│ └── 池化连接归还后的事务状态复位
42+
43+├── 6. 标准 SQL 分类
44+│ ├── DDL:CREATE / ALTER / TRUNCATE / DROP
45+│ ├── DML:INSERT / UPDATE / DELETE
46+│ ├── DQL:WHERE / ORDER BY / COUNT
47+│ ├── DCL:GRANT / REVOKE
48+│ └── TCL:COMMIT / ROLLBACK
49+
50+├── 7. 数据与协议兼容性
51+│ ├── 中文与 emoji
52+│ ├── 常用数值、日期和文本类型
53+│ ├── NULL 与 wasNull
54+│ ├── 多语句限制
55+│ ├── useCursorFetch 配置
56+│ └── useSSL=false 连接
57+
58+├── 8. 并发与稳定性
59+│ ├── 并发借出连接并同时持有
60+│ ├── 16 个工作线程执行完整事务 CRUD
61+│ ├── 128 条连接同时持有
62+│ ├── 256~5000 个业务请求排队复用连接池
63+│ ├── 重复创建和关闭连接池
64+│ ├── 长连接连续查询
65+│ └── 负载结束 active=0
66+
67+└── 9. 预期结果
68+ ├── 9 个测试类、46 个测试方法全部通过
69+ ├── Failures=0 / Errors=0 / Skipped=0
70+ └── 测试数据、临时角色和临时表全部清理
71+```
72+ 
73+## 测试主链路
74+ 
75+```text
76+dbcp.properties
77+
78+
79+BasicDataSourceFactory
80+
81+
82+BasicDataSource / GenericObjectPool
83+ │ 借出、校验、复用、归还
84+
85+MySQL Connector/J
86+ │ MySQL 协议握手、认证、SQL
87+
88+Dolphin MySQL 协议端口
89+
90+
91+openGauss B 兼容库
92+
93+
94+CRUD / 事务 / 五类 SQL / 并发与稳定性断言
95+```
@@ -0,0 +1,172 @@
1+# DBCP 连接 openGauss B 兼容模式自测报告
2+ 
3+## 1. 测试环境
4+ 
5+| 项目 | 配置 |
6+|---|---|
7+| 操作系统 | openEuler 24.03 x86_64 |
8+| openGauss | 7.0.0 master 转测包,`SELECT VERSION()` 显示 7.0.0-RC3 build a2d43dcd |
9+| Dolphin | MySQL 协议地址 `127.0.0.1:36443`(隔离自测实例;部署示例默认使用 `3308`) |
10+| JDK | BiSheng JDK 8u472 |
11+| Maven | 3.6.3 |
12+| DBCP2 / Pool2 | 2.9.0 / 2.10.0 |
13+| MySQL Connector/J | 8.0.28 |
14+| 数据库 / schema | `proto_test_db` / `mysql_test_db` |
15+ 
16+测试使用官方配套安装包中的 openGauss-server 和 Dolphin,连接账号为具备 DCL 测试权限的
17+`opengauss`。数据库密码通过 Maven 系统属性传入,未写入测试日志。`36443` 是隔离实例
18+端口,初始化步骤和示例配置默认使用 `3308`;运行测试时通过 `-Ddbcp.url` 覆盖为 `36443`
19+ 
20+## 2. 执行命令
21+ 
22+```bash
23+mvn clean test \
24+ -Ddbcp.url='jdbc:mysql://127.0.0.1:36443/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL' \
25+ -Ddbcp.username=opengauss \
26+ -Ddbcp.password="$DBCP_PASSWORD"
27+```
28+ 
29+## 3. 测试结果
30+ 
31+```text
32+Tests run: 46, Failures: 0, Errors: 0, Skipped: 0
33+BUILD SUCCESS
34+```
35+ 
36+| 测试类 | 用例数 | 结果 | 验证内容 |
37+|---|---:|---|---|
38+| `DbcpDataSourceTest` | 5 | PASS | 配置装配、驱动加载、初始连接和错误连接参数 |
39+| `ConnectionPoolTest` | 10 | PASS | 借出归还、池参数、并发、满池等待与恢复 |
40+| `ConnectionValidationTest` | 3 | PASS | 连接校验、失效连接淘汰和物理连接重建 |
41+| `CrudTest` | 8 | PASS | 查询、插入、更新、删除、分页和批处理 |
42+| `TransactionTest` | 5 | PASS | 自动提交、提交、回滚、隔离级别和池化连接事务 |
43+| `SqlCategoryTest` | 5 | PASS | DDL、DML、DQL、DCL、TCL |
44+| `DataTypeCharsetTest` | 3 | PASS | 中文、emoji、常用类型和 NULL |
45+| `ConstraintTest` | 3 | PASS | 多语句限制、游标参数和非 SSL 连接 |
46+| `StabilityTest` | 4 | PASS | 重复建池、并发事务、长连接和池状态 |
47+| **合计** | **46** | **PASS** | Failures、Errors、Skipped 均为 0 |
48+ 
49+## 4. 关键场景
50+ 
51+### 4.1 配置文件到连接池实例
52+ 
53+`DbcpDataSourceTest#testPoolConfigLoaded` 验证以下链路:
54+ 
55+```text
56+dbcp.properties
57+ -> TestConfig.load()
58+ -> BasicDataSourceFactory.create(Properties)
59+ -> BasicDataSource
60+```
61+ 
62+测试逐项读取并断言 URL、用户名、`initialSize``maxTotal``maxIdle``minIdle`
63+`maxWaitMillis``validationQuery``testOnBorrow``testWhileIdle`,确认配置文件中的参数
64+已应用到实际连接池实例。
65+ 
66+### 4.2 满池等待与恢复
67+ 
68+`ConnectionPoolTest#testMaxTotalConcurrentCrudAndQueuedBorrow` 设置 `maxTotal=8`
69+ 
70+1. 同时借出 8 条连接。
71+2. 每条连接执行 `INSERT -> SELECT -> UPDATE -> SELECT -> DELETE -> COMMIT` 并保持占用。
72+3. 第 9 条请求进入等待,活动连接数始终不超过 8。
73+4. 不释放连接时,第 9 条请求在 `maxWaitMillis` 后超时。
74+5. 释放连接后,新请求可以重新借到连接并执行 `SELECT 1`
75+6. 测试结束时 `numActive=0`
76+ 
77+### 4.3 并发业务
78+ 
79+`StabilityTest#testConcurrentCrudLoad` 使用 16 个工作线程,每个线程持有独立连接完成一次
80+事务 CRUD,所有线程提交后均清理测试数据,最终活动连接数为 0。
81+ 
82+高负载验证固定 `maxTotal=128`
83+ 
84+| 场景 | 结果 |
85+|---|---|
86+| 128 条连接同时执行事务 CRUD | 全部完成,结束 `active=0` |
87+| 256~5000 个业务请求排队复用 128 条连接 | 各档均完成,结束 `active=0` |
88+ 
89+请求数表示业务任务数量,连接池创建的物理连接数始终受 `maxTotal` 限制。
90+ 
91+### 4.4 事务
92+ 
93+| 场景 | 断言 |
94+|---|---|
95+| 自动提交 | 新连接的 `autoCommit``true` |
96+| 提交 | `commit()` 后,其他连接可以查询到已提交数据 |
97+| 回滚 | `rollback()` 后,其他连接查询不到回滚数据 |
98+| 隔离级别 | 可以设置并读取 `TRANSACTION_READ_COMMITTED``TRANSACTION_REPEATABLE_READ` |
99+| 池化连接 | 通过 DBCP 包装连接执行事务,归还后连接状态恢复 |
100+ 
101+### 4.5 DDL、DML、DQL、DCL、TCL
102+ 
103+| 分类 | 操作 | 断言 |
104+|---|---|---|
105+| DDL | `CREATE TABLE``ALTER TABLE``TRUNCATE TABLE``DROP TABLE` | 对象创建、结构变更、清空和删除成功 |
106+| DML | `INSERT``UPDATE``DELETE` | 影响行数和最终数据正确 |
107+| DQL | 条件查询、排序、`COUNT(*)` | 结果顺序、行数和聚合值正确 |
108+| DCL | `GRANT SELECT``REVOKE SELECT` | 授权后权限存在,撤销后权限消失 |
109+| TCL | `COMMIT``ROLLBACK` | 提交数据可见,回滚数据不可见 |
110+ 
111+DCL 用例使用临时角色和临时表,执行账号需要管理员权限或 `CREATEROLE` 权限。所有临时
112+对象均在测试结束时清理。
113+ 
114+### 4.6 断连恢复
115+ 
116+`ConnectionValidationTest` 通过独立管理连接终止被测服务端会话,然后验证:
117+ 
118+- `testOnBorrow=true` 时,借连接前发现失效连接并创建新的物理连接。
119+- `testWhileIdle=true` 时,维护线程从空闲池中清除失效连接。
120+- 新连接可以继续执行 `SELECT 1`
121+ 
122+## 5. Spring Boot 连接链路验证
123+ 
124+examples 工程新增 Spring Boot 2.5.6、DBCP2 2.9.0、Connector/J 8.0.28 的可运行链路示例。
125+代码在本地完成编译,并在 VM 的 master 转测包实例中完成启动验证:
126+ 
127+```text
128+SpringApplication.run()
129+ -> DataSourceAutoConfiguration
130+ -> BasicDataSource(DBCP2)/ PoolableConnection
131+ -> com.mysql.cj.jdbc.ConnectionImpl
132+ -> Dolphin MySQL 协议端口
133+ -> openGauss B 兼容库
134+ -> JdbcTemplate / TransactionTemplate
135+```
136+ 
137+运行时确认数据源类型和连接池参数;`JdbcTemplate` 完成查询,
138+`TransactionTemplate` 完成提交和回滚,临时数据清理成功。
139+ 
140+VM 内执行命令(密码通过环境变量传入,以下账号为临时测试账号):
141+ 
142+```bash
143+DBCP_URL='jdbc:mysql://127.0.0.1:36443/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL' \
144+DBCP_USER='<DBCP_USER>' DBCP_PASSWORD='<DBCP_PASSWORD>' mvn spring-boot:run
145+```
146+ 
147+VM 验证实例:openGauss 7.0.0 master 转测包(版本显示 7.0.0-RC3 build a2d43dcd),
148+PostgreSQL 端口 `36432`,Dolphin MySQL 协议端口 `36443`,JDK 1.8.0_472。
149+执行结果:
150+ 
151+```text
152+Spring 注入数据源: org.apache.commons.dbcp2.BasicDataSource
153+DBCP 参数: initialSize=2, maxTotal=8, maxWaitMillis=5000
154+JdbcTemplate 查询 user 表: 3 行
155+TransactionTemplate COMMIT 后查询到 1 行
156+TransactionTemplate ROLLBACK 后查询到 0 行
157+Spring Boot DBCP 链路验证完成,临时数据已清理
158+```
159+ 
160+该验证确认 Spring Boot 自动配置实际创建的是 DBCP `BasicDataSource`,并通过
161+`JdbcTemplate` 完成查询、`TransactionTemplate` 完成提交和回滚;不计入 DBCP 项目的
162+46 个低层测试方法。VM 验证使用专用测试账号,并为自增列授予了 schema 下序列权限:
163+ 
164+```sql
165+GRANT ALL ON ALL SEQUENCES IN SCHEMA mysql_test_db TO <DBCP_USER>;
166+```
167+ 
168+## 6. 结论
169+ 
170+在本报告所列环境中,DBCP2 2.9.0 可以通过 Connector/J 8.0.28 和 Dolphin MySQL 协议
171+连接 openGauss B 兼容库。连接池配置、CRUD、事务、满池等待、并发业务、断连恢复以及
172+DDL、DML、DQL、DCL、TCL 操作均通过验证。
@@ -0,0 +1,88 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<project xmlns="http://maven.apache.org/POM/4.0.0"
3+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+ <modelVersion>4.0.0</modelVersion>
6+ 
7+ <groupId>org.opengauss</groupId>
8+ <artifactId>dbcp-connect-opengauss-b</artifactId>
9+ <version>1.0.0</version>
10+ <packaging>jar</packaging>
11+ 
12+ <name>dbcp-connect-opengauss-b</name>
13+ <description>Apache Commons DBCP 连接池适配 openGauss B 兼容模式数据库示例</description>
14+ 
15+ <properties>
16+ <maven.compiler.source>1.8</maven.compiler.source>
17+ <maven.compiler.target>1.8</maven.compiler.target>
18+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
19+ <commons.dbcp2.version>2.9.0</commons.dbcp2.version>
20+ <mysql.connector.version>8.0.28</mysql.connector.version>
21+ <slf4j.version>1.7.36</slf4j.version>
22+ <junit.version>4.13.2</junit.version>
23+ <spring-boot.version>2.5.6</spring-boot.version>
24+ </properties>
25+ 
26+ <dependencies>
27+ <!-- Apache Commons DBCP2 连接池(含 commons-pool2 依赖) -->
28+ <dependency>
29+ <groupId>org.apache.commons</groupId>
30+ <artifactId>commons-dbcp2</artifactId>
31+ <version>${commons.dbcp2.version}</version>
32+ </dependency>
33+ 
34+ <!-- Spring Boot JDBC;排除默认 HikariCP,明确使用 DBCP2 -->
35+ <dependency>
36+ <groupId>org.springframework.boot</groupId>
37+ <artifactId>spring-boot-starter-jdbc</artifactId>
38+ <version>${spring-boot.version}</version>
39+ <exclusions>
40+ <exclusion>
41+ <groupId>com.zaxxer</groupId>
42+ <artifactId>HikariCP</artifactId>
43+ </exclusion>
44+ </exclusions>
45+ </dependency>
46+ 
47+ <!-- MySQL JDBC 驱动:本示例固定使用已验证的 8.0.28 -->
48+ <dependency>
49+ <groupId>mysql</groupId>
50+ <artifactId>mysql-connector-java</artifactId>
51+ <version>${mysql.connector.version}</version>
52+ </dependency>
53+ 
54+ <!-- 日志 -->
55+ <dependency>
56+ <groupId>org.slf4j</groupId>
57+ <artifactId>slf4j-api</artifactId>
58+ <version>${slf4j.version}</version>
59+ </dependency>
60+ <!-- 测试框架 -->
61+ <dependency>
62+ <groupId>junit</groupId>
63+ <artifactId>junit</artifactId>
64+ <version>${junit.version}</version>
65+ <scope>test</scope>
66+ </dependency>
67+ </dependencies>
68+ 
69+ <build>
70+ <plugins>
71+ <plugin>
72+ <groupId>org.apache.maven.plugins</groupId>
73+ <artifactId>maven-compiler-plugin</artifactId>
74+ <version>3.11.0</version>
75+ </plugin>
76+ <plugin>
77+ <groupId>org.apache.maven.plugins</groupId>
78+ <artifactId>maven-surefire-plugin</artifactId>
79+ <version>3.1.2</version>
80+ </plugin>
81+ <plugin>
82+ <groupId>org.springframework.boot</groupId>
83+ <artifactId>spring-boot-maven-plugin</artifactId>
84+ <version>${spring-boot.version}</version>
85+ </plugin>
86+ </plugins>
87+ </build>
88+</project>
@@ -0,0 +1,85 @@
1+package org.opengauss;
2+ 
3+import org.apache.commons.dbcp2.BasicDataSource;
4+import org.opengauss.dao.UserDao;
5+import org.opengauss.datasource.BasicDataSourceFactory;
6+import org.opengauss.entity.User;
7+import org.slf4j.Logger;
8+import org.slf4j.LoggerFactory;
9+ 
10+import java.util.Arrays;
11+import java.util.List;
12+ 
13+/**
14+ * DBCP 连接池连接 openGauss B 兼容模式数据库 —— 主示例程序。
15+ *
16+ * <p>运行前提:
17+ * <ol>
18+ * <li>openGauss 已开启 MySQL 协议兼容(enable_dolphin_proto=on),dolphin 监听端口可用</li>
19+ * <li>已创建 B 兼容库、业务用户并完成建表(见 src/main/resources/init.sql)</li>
20+ * <li>src/main/resources/dbcp.properties 中连接信息正确</li>
21+ * </ol>
22+ *
23+ * <p>演示流程:建池 → 查询 → 新增(自增主键回填)→ 更新 → 模糊/分页查询 → 批量插入 → 删除 → 关池。
24+ */
25+public class DbcpConnectDemo {
26+ 
27+ private static final Logger log = LoggerFactory.getLogger(DbcpConnectDemo.class);
28+ 
29+ public static void main(String[] args) throws Exception {
30+ // 1. 创建 DBCP 连接池
31+ BasicDataSource dataSource = BasicDataSourceFactory.create();
32+ log.info("连接池创建成功, initialSize={}, maxTotal={}",
33+ dataSource.getInitialSize(), dataSource.getMaxTotal());
34+ 
35+ UserDao userDao = new UserDao(dataSource);
36+ 
37+ try {
38+ // 2. 查询:全表
39+ log.info("===== 全表查询 =====");
40+ userDao.findAll().forEach(u -> log.info("{}", u));
41+ 
42+ // 3. 新增:自增主键回填
43+ log.info("===== 新增 =====");
44+ User newUser = new User("赵六", 21);
45+ int rows = userDao.insert(newUser);
46+ log.info("插入 {} 行, 回填主键 id={}", rows, newUser.getId());
47+ 
48+ // 4. 按 id 查询
49+ User found = userDao.findById(newUser.getId());
50+ log.info("按 id={} 查询结果: {}", newUser.getId(), found);
51+ 
52+ // 5. 更新
53+ log.info("===== 更新 =====");
54+ newUser.setAge(22);
55+ log.info("更新 {} 行", userDao.update(newUser));
56+ 
57+ // 6. 模糊查询
58+ log.info("===== 模糊查询 LIKE '%张%' =====");
59+ userDao.findByNameLike("张").forEach(u -> log.info("{}", u));
60+ 
61+ // 7. 分页查询
62+ log.info("===== 分页查询 LIMIT 2 OFFSET 1 =====");
63+ userDao.findByPage(2, 1).forEach(u -> log.info("{}", u));
64+ 
65+ // 8. 批量插入
66+ log.info("===== 批量插入 =====");
67+ List<User> batch = Arrays.asList(new User("钱七", 23), new User("孙八", 24));
68+ int[] batchRows = userDao.batchInsert(batch);
69+ log.info("批量插入影响行数: {}", Arrays.toString(batchRows));
70+ batch.forEach(u -> log.info("回填主键: {}", u));
71+ 
72+ // 9. 删除
73+ log.info("===== 删除 =====");
74+ log.info("删除 {} 行", userDao.delete(newUser.getId()));
75+ 
76+ // 10. 最终数据
77+ log.info("===== 最终全表数据 =====");
78+ userDao.findAll().forEach(u -> log.info("{}", u));
79+ } finally {
80+ // 11. 关闭连接池(释放全部连接)
81+ dataSource.close();
82+ log.info("连接池已关闭");
83+ }
84+ }
85+}
@@ -0,0 +1,84 @@
1+package org.opengauss;
2+ 
3+import org.apache.commons.dbcp2.BasicDataSource;
4+import org.slf4j.Logger;
5+import org.slf4j.LoggerFactory;
6+import org.springframework.boot.CommandLineRunner;
7+import org.springframework.boot.SpringApplication;
8+import org.springframework.boot.autoconfigure.SpringBootApplication;
9+import org.springframework.context.annotation.Bean;
10+import org.springframework.jdbc.core.JdbcTemplate;
11+import org.springframework.transaction.PlatformTransactionManager;
12+import org.springframework.transaction.support.TransactionTemplate;
13+ 
14+import javax.sql.DataSource;
15+import java.util.UUID;
16+ 
17+/**
18+ * Spring Boot + DBCP2 + MySQL Connector/J 连接 openGauss B 兼容库的可运行示例。
19+ *
20+ * <p>启动时会输出实际数据源类型,并通过 JdbcTemplate 执行查询、提交和回滚,
21+ * 用完连接后由 Spring/DBCP 自动归还连接池。</p>
22+ */
23+@SpringBootApplication
24+public class SpringBootDbcpApplication {
25+ 
26+ private static final Logger log = LoggerFactory.getLogger(SpringBootDbcpApplication.class);
27+ 
28+ public static void main(String[] args) {
29+ SpringApplication.run(SpringBootDbcpApplication.class, args);
30+ }
31+ 
32+ @Bean
33+ public CommandLineRunner verifyDbcp(DataSource dataSource, JdbcTemplate jdbcTemplate,
34+ PlatformTransactionManager transactionManager) {
35+ return args -> {
36+ if (!(dataSource instanceof BasicDataSource)) {
37+ throw new IllegalStateException("实际数据源不是 DBCP BasicDataSource: "
38+ + dataSource.getClass().getName());
39+ }
40+ BasicDataSource dbcp = (BasicDataSource) dataSource;
41+ log.info("Spring 注入数据源: {}", dataSource.getClass().getName());
42+ log.info("DBCP 参数: initialSize={}, maxTotal={}, maxWaitMillis={}",
43+ dbcp.getInitialSize(), dbcp.getMaxTotal(), dbcp.getMaxWaitMillis());
44+ 
45+ Integer initialRows = jdbcTemplate.queryForObject(
46+ "SELECT COUNT(*) FROM `user`", Integer.class);
47+ log.info("JdbcTemplate 查询 user 表: {} 行", initialRows);
48+ 
49+ String marker = "spring-" + UUID.randomUUID().toString().replace("-", "");
50+ String rollbackMarker = marker + "-rollback";
51+ TransactionTemplate transactions = new TransactionTemplate(transactionManager);
52+ try {
53+ // 提交事务:插入后由另一个连接可见,证明 Spring 事务管理器已生效。
54+ transactions.execute(status -> {
55+ jdbcTemplate.update("INSERT INTO `user`(`name`,`age`) VALUES (?,?)", marker, 30);
56+ return null;
57+ });
58+ Integer committed = jdbcTemplate.queryForObject(
59+ "SELECT COUNT(*) FROM `user` WHERE `name` = ?", Integer.class, marker);
60+ if (!Integer.valueOf(1).equals(committed)) {
61+ throw new IllegalStateException("提交事务后的数据行数应为 1,实际为 " + committed);
62+ }
63+ log.info("TransactionTemplate COMMIT 后查询到 {} 行", committed);
64+ 
65+ // 回滚事务:标记回滚后不得留下数据。
66+ transactions.execute(status -> {
67+ jdbcTemplate.update("INSERT INTO `user`(`name`,`age`) VALUES (?,?)",
68+ rollbackMarker, 31);
69+ status.setRollbackOnly();
70+ return null;
71+ });
72+ Integer rolledBack = jdbcTemplate.queryForObject(
73+ "SELECT COUNT(*) FROM `user` WHERE `name` = ?", Integer.class, rollbackMarker);
74+ if (!Integer.valueOf(0).equals(rolledBack)) {
75+ throw new IllegalStateException("回滚事务后的数据行数应为 0,实际为 " + rolledBack);
76+ }
77+ log.info("TransactionTemplate ROLLBACK 后查询到 {} 行", rolledBack);
78+ } finally {
79+ jdbcTemplate.update("DELETE FROM `user` WHERE `name` IN (?, ?)", marker, rollbackMarker);
80+ log.info("Spring Boot DBCP 链路验证完成,临时数据已清理");
81+ }
82+ };
83+ }
84+}
@@ -0,0 +1,174 @@
1+package org.opengauss.dao;
2+ 
3+import org.opengauss.entity.User;
4+ 
5+import javax.sql.DataSource;
6+import java.sql.Connection;
7+import java.sql.PreparedStatement;
8+import java.sql.ResultSet;
9+import java.sql.SQLException;
10+import java.sql.Statement;
11+import java.util.ArrayList;
12+import java.util.List;
13+ 
14+/**
15+ * 基于 JDBC 手写 SQL 的 DAO,演示 DBCP 连接池下的 CRUD / 分页 / 批量操作。
16+ * 所有连接均从连接池(DataSource)获取,用完即还,由 DBCP 统一管理。
17+ */
18+public class UserDao {
19+ 
20+ private final DataSource dataSource;
21+ 
22+ public UserDao(DataSource dataSource) {
23+ this.dataSource = dataSource;
24+ }
25+ 
26+ /**
27+ * 新增用户,并回填自增主键(AUTO_INCREMENT)
28+ */
29+ public int insert(User user) throws SQLException {
30+ String sql = "INSERT INTO `user`(`name`,`age`) VALUES (?,?)";
31+ try (Connection conn = dataSource.getConnection();
32+ PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
33+ ps.setString(1, user.getName());
34+ ps.setInt(2, user.getAge());
35+ int rows = ps.executeUpdate();
36+ try (ResultSet keys = ps.getGeneratedKeys()) {
37+ if (keys.next()) {
38+ user.setId(keys.getInt(1));
39+ }
40+ }
41+ return rows;
42+ }
43+ }
44+ 
45+ /**
46+ * 按 id 查询
47+ */
48+ public User findById(int id) throws SQLException {
49+ String sql = "SELECT id,name,age FROM `user` WHERE id=?";
50+ try (Connection conn = dataSource.getConnection();
51+ PreparedStatement ps = conn.prepareStatement(sql)) {
52+ ps.setInt(1, id);
53+ try (ResultSet rs = ps.executeQuery()) {
54+ if (rs.next()) {
55+ return toUser(rs);
56+ }
57+ }
58+ }
59+ return null;
60+ }
61+ 
62+ /**
63+ * 全表查询
64+ */
65+ public List<User> findAll() throws SQLException {
66+ String sql = "SELECT id,name,age FROM `user` ORDER BY id";
67+ List<User> list = new ArrayList<>();
68+ try (Connection conn = dataSource.getConnection();
69+ PreparedStatement ps = conn.prepareStatement(sql);
70+ ResultSet rs = ps.executeQuery()) {
71+ while (rs.next()) {
72+ list.add(toUser(rs));
73+ }
74+ }
75+ return list;
76+ }
77+ 
78+ /**
79+ * 模糊查询(LIKE)
80+ */
81+ public List<User> findByNameLike(String keyword) throws SQLException {
82+ String sql = "SELECT id,name,age FROM `user` WHERE name LIKE ? ORDER BY id";
83+ List<User> list = new ArrayList<>();
84+ try (Connection conn = dataSource.getConnection();
85+ PreparedStatement ps = conn.prepareStatement(sql)) {
86+ ps.setString(1, "%" + keyword + "%");
87+ try (ResultSet rs = ps.executeQuery()) {
88+ while (rs.next()) {
89+ list.add(toUser(rs));
90+ }
91+ }
92+ }
93+ return list;
94+ }
95+ 
96+ /**
97+ * 分页查询(LIMIT/OFFSET)
98+ */
99+ public List<User> findByPage(int limit, int offset) throws SQLException {
100+ String sql = "SELECT id,name,age FROM `user` ORDER BY id LIMIT ? OFFSET ?";
101+ List<User> list = new ArrayList<>();
102+ try (Connection conn = dataSource.getConnection();
103+ PreparedStatement ps = conn.prepareStatement(sql)) {
104+ ps.setInt(1, limit);
105+ ps.setInt(2, offset);
106+ try (ResultSet rs = ps.executeQuery()) {
107+ while (rs.next()) {
108+ list.add(toUser(rs));
109+ }
110+ }
111+ }
112+ return list;
113+ }
114+ 
115+ /**
116+ * 更新用户
117+ */
118+ public int update(User user) throws SQLException {
119+ String sql = "UPDATE `user` SET name=?, age=? WHERE id=?";
120+ try (Connection conn = dataSource.getConnection();
121+ PreparedStatement ps = conn.prepareStatement(sql)) {
122+ ps.setString(1, user.getName());
123+ ps.setInt(2, user.getAge());
124+ ps.setInt(3, user.getId());
125+ return ps.executeUpdate();
126+ }
127+ }
128+ 
129+ /**
130+ * 删除用户
131+ */
132+ public int delete(int id) throws SQLException {
133+ String sql = "DELETE FROM `user` WHERE id=?";
134+ try (Connection conn = dataSource.getConnection();
135+ PreparedStatement ps = conn.prepareStatement(sql)) {
136+ ps.setInt(1, id);
137+ return ps.executeUpdate();
138+ }
139+ }
140+ 
141+ /**
142+ * 批量插入(批量执行 + 自增主键回填)
143+ */
144+ public int[] batchInsert(List<User> users) throws SQLException {
145+ String sql = "INSERT INTO `user`(`name`,`age`) VALUES (?,?)";
146+ try (Connection conn = dataSource.getConnection();
147+ PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
148+ for (User user : users) {
149+ ps.setString(1, user.getName());
150+ ps.setInt(2, user.getAge());
151+ ps.addBatch();
152+ }
153+ int[] rows = ps.executeBatch();
154+ try (ResultSet keys = ps.getGeneratedKeys()) {
155+ int i = 0;
156+ while (keys.next()) {
157+ if (i < users.size()) {
158+ users.get(i).setId(keys.getInt(1));
159+ }
160+ i++;
161+ }
162+ }
163+ return rows;
164+ }
165+ }
166+ 
167+ private User toUser(ResultSet rs) throws SQLException {
168+ User user = new User();
169+ user.setId(rs.getInt("id"));
170+ user.setName(rs.getString("name"));
171+ user.setAge(rs.getInt("age"));
172+ return user;
173+ }
174+}
@@ -0,0 +1,84 @@
1+package org.opengauss.datasource;
2+ 
3+import org.apache.commons.dbcp2.BasicDataSource;
4+import org.slf4j.Logger;
5+import org.slf4j.LoggerFactory;
6+ 
7+import java.io.IOException;
8+import java.io.InputStream;
9+import java.util.Properties;
10+ 
11+/**
12+ * DBCP 连接池工厂。
13+ * 支持两种创建方式:
14+ * 1. 编程式:直接调用 {@link #create(Properties)} 传入配置
15+ * 2. 配置式:从 classpath 下的 dbcp.properties 加载配置 {@link #create()}
16+ */
17+public class BasicDataSourceFactory {
18+ 
19+ private static final Logger log = LoggerFactory.getLogger(BasicDataSourceFactory.class);
20+ 
21+ private BasicDataSourceFactory() {
22+ }
23+ 
24+ /**
25+ * 从 classpath 加载 dbcp.properties 并创建连接池
26+ */
27+ public static BasicDataSource create() {
28+ return create(loadProperties());
29+ }
30+ 
31+ /**
32+ * 根据配置创建连接池
33+ */
34+ public static BasicDataSource create(Properties props) {
35+ BasicDataSource ds = new BasicDataSource();
36+ 
37+ // ---- 驱动与连接 ----
38+ ds.setDriverClassName(props.getProperty("driverClassName"));
39+ ds.setUrl(props.getProperty("url"));
40+ ds.setUsername(props.getProperty("username"));
41+ ds.setPassword(props.getProperty("password"));
42+ 
43+ // ---- 池容量参数 ----
44+ ds.setInitialSize(Integer.parseInt(props.getProperty("initialSize", "2")));
45+ ds.setMaxTotal(Integer.parseInt(props.getProperty("maxTotal", "8")));
46+ ds.setMaxIdle(Integer.parseInt(props.getProperty("maxIdle", "8")));
47+ ds.setMinIdle(Integer.parseInt(props.getProperty("minIdle", "2")));
48+ ds.setMaxWaitMillis(Long.parseLong(props.getProperty("maxWaitMillis", "5000")));
49+ 
50+ // ---- 连接校验 ----
51+ ds.setValidationQuery(props.getProperty("validationQuery", "SELECT 1"));
52+ ds.setTestOnBorrow(Boolean.parseBoolean(props.getProperty("testOnBorrow", "true")));
53+ ds.setTestWhileIdle(Boolean.parseBoolean(props.getProperty("testWhileIdle", "true")));
54+ ds.setTimeBetweenEvictionRunsMillis(
55+ Long.parseLong(props.getProperty("timeBetweenEvictionRunsMillis", "60000")));
56+ ds.setMinEvictableIdleTimeMillis(
57+ Long.parseLong(props.getProperty("minEvictableIdleTimeMillis", "300000")));
58+ 
59+ // ---- 防连接泄漏 ----
60+ ds.setRemoveAbandonedOnBorrow(Boolean.parseBoolean(props.getProperty("removeAbandonedOnBorrow", "false")));
61+ ds.setRemoveAbandonedTimeout(Integer.parseInt(props.getProperty("removeAbandonedTimeout", "300")));
62+ 
63+ log.info("DBCP 连接池配置完成: url={}, initialSize={}, maxTotal={}",
64+ ds.getUrl(), ds.getInitialSize(), ds.getMaxTotal());
65+ return ds;
66+ }
67+ 
68+ /**
69+ * 加载 dbcp.properties 配置文件
70+ */
71+ private static Properties loadProperties() {
72+ Properties props = new Properties();
73+ try (InputStream in = BasicDataSourceFactory.class.getClassLoader()
74+ .getResourceAsStream("dbcp.properties")) {
75+ if (in == null) {
76+ throw new IllegalStateException("找不到配置文件 dbcp.properties,请确认它在 classpath 下");
77+ }
78+ props.load(in);
79+ } catch (IOException e) {
80+ throw new IllegalStateException("加载 dbcp.properties 失败", e);
81+ }
82+ return props;
83+ }
84+}
@@ -0,0 +1,51 @@
1+package org.opengauss.entity;
2+ 
3+/**
4+ * 用户实体类,对应 B 兼容库 mysql_test_db 下的 user 表
5+ */
6+public class User {
7+ 
8+ /** 自增主键 */
9+ private Integer id;
10+ /** 用户名 */
11+ private String name;
12+ /** 年龄 */
13+ private Integer age;
14+ 
15+ public User() {
16+ }
17+ 
18+ public User(String name, Integer age) {
19+ this.name = name;
20+ this.age = age;
21+ }
22+ 
23+ public Integer getId() {
24+ return id;
25+ }
26+ 
27+ public void setId(Integer id) {
28+ this.id = id;
29+ }
30+ 
31+ public String getName() {
32+ return name;
33+ }
34+ 
35+ public void setName(String name) {
36+ this.name = name;
37+ }
38+ 
39+ public Integer getAge() {
40+ return age;
41+ }
42+ 
43+ public void setAge(Integer age) {
44+ this.age = age;
45+ }
46+ 
47+ @Override
48+ public String toString() {
49+ return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
50+ }
51+}
@@ -0,0 +1,21 @@
1+spring.application.name=dbcp-connect-opengauss-b
2+ 
3+# Spring Boot 默认会优先选择 HikariCP;pom.xml 已排除 HikariCP,下面再显式指定 DBCP2。
4+spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
5+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
6+spring.datasource.url=${DBCP_URL:jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL}
7+spring.datasource.username=${DBCP_USER:opengauss}
8+spring.datasource.password=${DBCP_PASSWORD:change_me}
9+ 
10+# DBCP2 连接池参数
11+spring.datasource.dbcp2.initial-size=2
12+spring.datasource.dbcp2.max-total=8
13+spring.datasource.dbcp2.max-idle=8
14+spring.datasource.dbcp2.min-idle=2
15+spring.datasource.dbcp2.max-wait-millis=5000
16+spring.datasource.dbcp2.validation-query=SELECT 1
17+spring.datasource.dbcp2.test-on-borrow=true
18+spring.datasource.dbcp2.test-while-idle=true
19+ 
20+# 业务示例由代码显式执行 SQL,不让 Spring Boot 自动执行未审核的脚本。
21+spring.sql.init.mode=never
@@ -0,0 +1,31 @@
1+-- =====================================================================
2+-- 第 1 步:创建 B 兼容数据库 + 业务 schema + MySQL 协议 native 密码
3+-- 用法(在 postgres 默认库中执行,当前用户为超级用户,如 opengauss):
4+-- gsql -d postgres -h <unix_socket目录> -p 5432 -f create_b_database.sql
5+-- 执行顺序:本脚本必须在 init.sql 之前执行(先建库,再在库里建表)。
6+--
7+-- 幂等说明:
8+-- - 首次在空环境执行:全部语句一次成功(CREATE DATABASE / schema / 授权)。
9+-- - 重复执行:仅 CREATE DATABASE 可能报 “already exists”(gsql 不支持 \gexec,
10+-- 且 CREATE DATABASE 不能放入 DO 块);CREATE SCHEMA 使用 IF NOT EXISTS,
11+-- 重复执行不报错。重复建表由 init.sql 的 IF NOT EXISTS 与条件插入保证幂等。
12+-- =====================================================================
13+ 
14+-- 1. 创建 B 兼容数据库(DBCOMPATIBILITY 'B',提供 utf8mb4 / MySQL 语义)
15+CREATE DATABASE proto_test_db WITH DBCOMPATIBILITY = 'B';
16+ 
17+-- 2. 连接进入 B 兼容库(后续 schema / 密码均在该库上下文执行)
18+\connect proto_test_db
19+ 
20+-- 3. 设置 MySQL 协议 native 密码。执行前将 <DBCP_PASSWORD> 替换为实际密码,
21+-- 并与 dbcp.properties 中的 password 保持一致。第三个参数是原 native 密码;
22+-- 首次设置或重复使用同一密码时可保持如下写法,修改已有密码时应改为当前密码。
23+SELECT set_native_password('opengauss', '<DBCP_PASSWORD>', '<DBCP_PASSWORD>');
24+ 
25+-- 4. 创建业务 schema(MySQL 协议下 schema 等价于 database,
26+-- jdbc:mysql://<host>:3308/mysql_test_db 中的库名即映射到该 schema)
27+CREATE SCHEMA IF NOT EXISTS mysql_test_db;
28+ 
29+-- 5. 表权限:schema 与其中表对象默认属于当前超级用户 opengauss;
30+-- 显式授权给 opengauss,保证 DBCP 连接(opengauss 账号)可读写
31+GRANT ALL ON SCHEMA mysql_test_db TO opengauss;
@@ -0,0 +1,26 @@
1+# ============ 驱动与连接 ============
2+# MySQL JDBC 驱动(本示例固定使用已验证的 8.0.28)
3+driverClassName=com.mysql.cj.jdbc.Driver
4+# dolphin 协议监听端口 + B 兼容库下的 schema 名
5+# 本地示例的 Dolphin MySQL 协议端口未启用 SSL,因此设置 useSSL=false
6+url=jdbc:mysql://127.0.0.1:3308/mysql_test_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL
7+username=opengauss
8+password=<DBCP_PASSWORD>
9+ 
10+# ============ 池容量参数 ============
11+initialSize=2
12+maxTotal=8
13+maxIdle=8
14+minIdle=2
15+maxWaitMillis=5000
16+ 
17+# ============ 连接校验 ============
18+validationQuery=SELECT 1
19+testOnBorrow=true
20+testWhileIdle=true
21+timeBetweenEvictionRunsMillis=60000
22+minEvictableIdleTimeMillis=300000
23+ 
24+# ============ 防连接泄漏 ============
25+removeAbandonedOnBorrow=false
26+removeAbandonedTimeout=300
@@ -0,0 +1,26 @@
1+-- =====================================================================
2+-- 第 2 步:在 B 兼容库 proto_test_db 中创建业务表并初始化数据
3+-- 用法(需先执行 create_b_database.sql,再连接进入 proto_test_db):
4+-- gsql -d proto_test_db -h <unix_socket目录> -p 5432 -f init.sql
5+--
6+-- 幂等:建表使用 IF NOT EXISTS,初始数据按姓名去重,可重复执行。
7+-- =====================================================================
8+ 
9+-- 1. 切换业务 schema(MySQL 协议下 schema 等价于 database,
10+-- 与 jdbc:mysql://<host>:3308/mysql_test_db 的 URL 库名一致)
11+SET current_schema TO mysql_test_db;
12+ 
13+-- 2. 创建业务表(与官网 Mybatis 示例保持一致,便于横向对比)
14+CREATE TABLE IF NOT EXISTS `user` (
15+ `id` INT AUTO_INCREMENT PRIMARY KEY,
16+ `name` VARCHAR(50) NOT NULL COMMENT '用户名',
17+ `age` INT COMMENT '年龄'
18+) DEFAULT CHARSET=utf8mb4;
19+ 
20+-- 3. 初始化数据(幂等:仅当姓名不存在时插入)
21+INSERT INTO `user` (`name`,`age`)
22+SELECT '张三', 18 WHERE NOT EXISTS (SELECT 1 FROM `user` WHERE `name` = '张三');
23+INSERT INTO `user` (`name`,`age`)
24+SELECT '李四', 19 WHERE NOT EXISTS (SELECT 1 FROM `user` WHERE `name` = '李四');
25+INSERT INTO `user` (`name`,`age`)
26+SELECT '王五', 20 WHERE NOT EXISTS (SELECT 1 FROM `user` WHERE `name` = '王五');
@@ -0,0 +1,229 @@
1+package org.opengauss.test;
2+ 
3+import org.apache.commons.dbcp2.BasicDataSource;
4+import org.opengauss.datasource.BasicDataSourceFactory;
5+import org.opengauss.test.util.TestConfig;
6+import org.junit.AfterClass;
7+import org.junit.BeforeClass;
8+ 
9+import java.sql.Connection;
10+import java.sql.ResultSet;
11+import java.sql.SQLException;
12+import java.sql.Statement;
13+import java.util.Properties;
14+import java.util.concurrent.ExecutorService;
15+import java.util.concurrent.TimeUnit;
16+ 
17+import static org.junit.Assert.assertTrue;
18+import static org.junit.Assert.fail;
19+ 
20+/**
21+ * 测试基类:
22+ * 1. 统一创建 / 关闭连接池(每个测试类各自建池,互不影响)
23+ * 2. 提供把连接参数应用到临时 BasicDataSource 的工具方法
24+ * 3. 提供数据库连通性预检(失败时给出明确的排查提示)
25+ * 4. 提供测试共用的连接池/物理连接/会话断言工具方法
26+ */
27+public abstract class BaseDbTest {
28+ 
29+ protected static BasicDataSource dataSource;
30+ 
31+ @BeforeClass
32+ public static void initPool() throws Exception {
33+ closeIfNeeded();
34+ dataSource = BasicDataSourceFactory.create(TestConfig.load());
35+ // 连通性预检:能借出连接并执行 SELECT 1 才继续,否则给出明确提示
36+ try (Connection conn = dataSource.getConnection();
37+ Statement st = conn.createStatement()) {
38+ st.execute("SELECT 1");
39+ } catch (Exception e) {
40+ closeIfNeeded();
41+ throw new IllegalStateException(
42+ "数据库不可达,请检查:① openGauss 实例是否已启动并开启 enable_dolphin_proto;"
43+ + "② dolphin_server_port 与 dbcp.properties 中 url 端口是否一致;"
44+ + "③ 用户/密码/权限是否正确。原始异常: " + e.getMessage(), e);
45+ }
46+ }
47+ 
48+ @AfterClass
49+ public static void closePool() throws Exception {
50+ closeIfNeeded();
51+ }
52+ 
53+ private static void closeIfNeeded() {
54+ if (dataSource != null) {
55+ try {
56+ dataSource.close();
57+ } catch (SQLException ignored) {
58+ // 关闭失败不影响用例结果
59+ }
60+ }
61+ }
62+ 
63+ /**
64+ * 把 dbcp.properties(含系统属性覆盖)的连接参数应用到临时连接池
65+ */
66+ protected static void applyUrl(BasicDataSource ds) {
67+ Properties p = TestConfig.load();
68+ ds.setDriverClassName(p.getProperty("driverClassName"));
69+ ds.setUrl(p.getProperty("url"));
70+ ds.setUsername(p.getProperty("username"));
71+ ds.setPassword(p.getProperty("password"));
72+ }
73+ 
74+ /**
75+ * 新建一个仅带连接参数(驱动/URL/账号/密码)的测试专用连接池。
76+ * 其余池参数由各用例显式设置,避免依赖 dbcp.properties 的全局取值。
77+ */
78+ protected static BasicDataSource newPool() {
79+ BasicDataSource ds = new BasicDataSource();
80+ applyUrl(ds);
81+ return ds;
82+ }
83+ 
84+ /**
85+ * 获取当前 MySQL 会话的服务端会话标识(SELECT CONNECTION_ID())。
86+ * 注意:dolphin 会在会话结束后复用该数值,故只用于“同一会话复用”判断,
87+ * 不能用于区分“重建后的新会话”是否不同。
88+ */
89+ protected static long connectionId(Connection conn) throws SQLException {
90+ try (Statement st = conn.createStatement();
91+ ResultSet rs = st.executeQuery("SELECT CONNECTION_ID()")) {
92+ if (!rs.next()) {
93+ throw new SQLException("SELECT CONNECTION_ID() 无返回结果");
94+ }
95+ return rs.getLong(1);
96+ }
97+ }
98+ 
99+ /**
100+ * 获取被 DBCP 包装的真实 MySQL 物理连接。
101+ * 不使用 unwrap(Connection.class)——它只会返回 DBCP 包装对象本身。
102+ */
103+ protected static com.mysql.cj.jdbc.ConnectionImpl physicalConnection(Connection conn) throws SQLException {
104+ com.mysql.cj.jdbc.ConnectionImpl phys = conn.unwrap(com.mysql.cj.jdbc.ConnectionImpl.class);
105+ if (phys == null) {
106+ throw new SQLException("无法取得真实 MySQL 物理连接(unwrap(ConnectionImpl.class) 返回 null)");
107+ }
108+ return phys;
109+ }
110+ 
111+ /**
112+ * 服务端断连:从独立管理连接(共享池 dataSource 的另一个会话)执行 KILL <CONNECTION_ID>。
113+ * 不调用被测连接的 ConnectionImpl.close(),被测 JDBC/物理连接本地仍报告 isClosed()==false
114+ * 从而证明是服务端终止会话,而非客户端关闭。
115+ * 说明:dolphin 的 MySQL 协议实测支持 KILL(本环境已验证:执行后服务端会话从
116+ * SHOW PROCESSLIST 消失,客户端 isClosed() 仍为 false,下次查询才报断连)。
117+ */
118+ protected static void killServerSession(long id) throws SQLException {
119+ try (Connection mgmt = dataSource.getConnection();
120+ Statement st = mgmt.createStatement()) {
121+ st.execute("KILL " + id);
122+ }
123+ }
124+ 
125+ /**
126+ * 通过 SHOW PROCESSLIST 确认某个服务端会话是否仍存在(用于核验服务端断连是否真正生效)。
127+ */
128+ protected static boolean serverSessionExists(long id) throws SQLException {
129+ try (Connection c = dataSource.getConnection();
130+ Statement st = c.createStatement();
131+ ResultSet rs = st.executeQuery("SHOW PROCESSLIST")) {
132+ while (rs.next()) {
133+ if (rs.getLong(1) == id) {
134+ return true;
135+ }
136+ }
137+ }
138+ return false;
139+ }
140+ 
141+ /**
142+ * 停止并等待线程池终止(带超时):shutdown 后 awaitTermination 指定时长,
143+ * 超时仍未终止则 shutdownNow 再补等一次;仍无法终止视为测试失败。
144+ * 保证所有并发用例的线程终止路径都有明确超时,不会无限挂起。
145+ */
146+ protected static void shutdownExecutor(ExecutorService pool, long timeoutMillis, String message)
147+ throws InterruptedException {
148+ if (pool == null) {
149+ return;
150+ }
151+ pool.shutdown();
152+ if (!pool.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS)) {
153+ pool.shutdownNow();
154+ if (!pool.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS)) {
155+ fail(message + "(线程池在超时内未终止)");
156+ }
157+ }
158+ }
159+ 
160+ /**
161+ * 先带超时停止线程池,随后无论线程池终止是否失败都关闭连接池;
162+ * 线程池终止失败/被中断不会阻止 ds.close(),其失败会在连接池关闭后重新抛出。
163+ */
164+ protected static void shutdownExecutorAndClosePool(ExecutorService pool, BasicDataSource ds,
165+ long timeoutMillis, String message) {
166+ Throwable failure = null;
167+ try {
168+ shutdownExecutor(pool, timeoutMillis, message);
169+ } catch (Throwable t) {
170+ failure = t;
171+ }
172+ closeDataSourceQuietly(ds);
173+ if (failure != null) {
174+ if (failure instanceof InterruptedException) {
175+ Thread.currentThread().interrupt();
176+ }
177+ if (failure instanceof RuntimeException) {
178+ throw (RuntimeException) failure;
179+ }
180+ if (failure instanceof Error) {
181+ throw (Error) failure;
182+ }
183+ throw new RuntimeException(failure);
184+ }
185+ }
186+ 
187+ /**
188+ * 关闭测试专用连接池(忽略关闭异常,确保 finally 中不因关闭失败掩盖原断言失败)。
189+ */
190+ protected static void closeDataSourceQuietly(BasicDataSource ds) {
191+ if (ds != null) {
192+ try {
193+ ds.close();
194+ } catch (SQLException ignored) {
195+ }
196+ }
197+ }
198+ 
199+ /**
200+ * 关闭单个连接(忽略关闭异常)。finally 中逐个调用:某个连接关闭失败不影响其余连接与连接池关闭。
201+ */
202+ protected static void closeConnectionQuietly(Connection c) {
203+ if (c != null) {
204+ try {
205+ c.close();
206+ } catch (SQLException ignored) {
207+ }
208+ }
209+ }
210+ 
211+ /**
212+ * 轮询等待条件成立(带超时,避免测试无限阻塞)。
213+ */
214+ protected static void waitForCondition(Condition cond, long timeoutMillis, String message) throws InterruptedException {
215+ long deadline = System.currentTimeMillis() + timeoutMillis;
216+ while (System.currentTimeMillis() < deadline) {
217+ if (cond.eval()) {
218+ return;
219+ }
220+ Thread.sleep(200);
221+ }
222+ fail(message);
223+ }
224+ 
225+ @FunctionalInterface
226+ protected interface Condition {
227+ boolean eval();
228+ }
229+}
@@ -0,0 +1,498 @@
1+package org.opengauss.test;
2+ 
3+import org.apache.commons.dbcp2.BasicDataSource;
4+import org.junit.Test;
5+ 
6+import java.sql.Connection;
7+import java.sql.PreparedStatement;
8+import java.sql.ResultSet;
9+import java.sql.SQLException;
10+import java.sql.Statement;
11+import java.util.ArrayList;
12+import java.util.List;
13+import java.util.NoSuchElementException;
14+import java.util.concurrent.CountDownLatch;
15+import java.util.concurrent.ExecutorService;
16+import java.util.concurrent.Executors;
17+import java.util.concurrent.Future;
18+import java.util.concurrent.TimeUnit;
19+ 
20+import static org.junit.Assert.assertEquals;
21+import static org.junit.Assert.assertFalse;
22+import static org.junit.Assert.assertTrue;
23+import static org.junit.Assert.fail;
24+ 
25+/**
26+ * 连接池核心行为测试:
27+ * 并发借出(两阶段屏障 + 全部同时持有)、maxTotal/maxWaitMillis 上下界与池耗尽异常、
28+ * maxIdle 对空闲连接数上限(精确收敛)、minIdle 由维护线程补足、归还后物理会话复用、
29+ * 连接状态复位(autoCommit/隔离级别)、removeAbandoned 泄漏回收、按空闲时长驱逐。
30+ */
31+public class ConnectionPoolTest extends BaseDbTest {
32+ 
33+ @Test
34+ public void testBorrowAndReturn() throws Exception {
35+ try (Connection conn = dataSource.getConnection()) {
36+ assertFalse(conn.isClosed());
37+ assertEquals(1, dataSource.getNumActive());
38+ }
39+ assertEquals("归还后 active 应为 0", 0, dataSource.getNumActive());
40+ }
41+ 
42+ /**
43+ * 并发借出:使用测试专用连接池 + 两阶段屏障。
44+ * 所有线程先各自借到连接并一直持有,主线程确认 active 达到线程数(同时持有的峰值),
45+ * 再统一放行执行查询。所有 await/Future 均带超时。
46+ */
47+ @Test
48+ public void testConcurrentBorrowAllThreadsHoldSimultaneously() throws Exception {
49+ int threads = 6;
50+ BasicDataSource ds = newPool();
51+ ds.setMaxTotal(threads);
52+ ds.setMaxWaitMillis(5000);
53+ ExecutorService pool = Executors.newFixedThreadPool(threads);
54+ try {
55+ CountDownLatch held = new CountDownLatch(threads); // 阶段1:所有线程已借到连接
56+ CountDownLatch release = new CountDownLatch(1); // 阶段2:放行执行查询
57+ CountDownLatch done = new CountDownLatch(threads);
58+ List<Future<Boolean>> results = new ArrayList<>();
59+ for (int i = 0; i < threads; i++) {
60+ results.add(pool.submit(() -> {
61+ try (Connection conn = ds.getConnection()) {
62+ held.countDown();
63+ if (!release.await(15, TimeUnit.SECONDS)) {
64+ return false;
65+ }
66+ try (Statement st = conn.createStatement();
67+ ResultSet rs = st.executeQuery("SELECT 1")) {
68+ return rs.next() && rs.getInt(1) == 1;
69+ }
70+ } finally {
71+ done.countDown();
72+ }
73+ }));
74+ }
75+ assertTrue("所有线程应在 15s 内借到连接", held.await(15, TimeUnit.SECONDS));
76+ 
77+ // 所有线程此刻都持有连接且阻塞在屏障上,active 应正好等于线程数(即并发峰值)
78+ int peakActive = 0;
79+ for (int i = 0; i < 50; i++) {
80+ peakActive = Math.max(peakActive, ds.getNumActive());
81+ if (ds.getNumActive() >= threads) {
82+ break;
83+ }
84+ Thread.sleep(20);
85+ }
86+ assertEquals("所有线程同时持有连接时 peak active 应等于线程数", threads, peakActive);
87+ assertEquals("屏障阶段 active 应等于线程数(全部同时持有)", threads, ds.getNumActive());
88+ 
89+ release.countDown();
90+ assertTrue("所有线程应在 15s 内完成查询并归还", done.await(15, TimeUnit.SECONDS));
91+ for (Future<Boolean> f : results) {
92+ assertTrue("并发借出执行失败", f.get(5, TimeUnit.SECONDS));
93+ }
94+ assertEquals("全部归还后 active 应为 0", 0, ds.getNumActive());
95+ } finally {
96+ shutdownExecutorAndClosePool(pool, ds, 15000, "并发借出线程未在超时内终止");
97+ }
98+ }
99+ 
100+ /**
101+ * maxTotal/maxWaitMillis:验证等待时长上下界与明确的池耗尽异常。
102+ * 若第二次借出意外成功,必须先关闭该连接再 fail;连接池在 finally 中可靠关闭。
103+ */
104+ @Test
105+ public void testMaxTotalAndMaxWaitTimeout() throws Exception {
106+ BasicDataSource ds = newPool();
107+ ds.setMaxTotal(1);
108+ ds.setMaxWaitMillis(2000);
109+ Connection c2 = null;
110+ try {
111+ try (Connection c1 = ds.getConnection()) {
112+ long begin = System.currentTimeMillis();
113+ try {
114+ c2 = ds.getConnection();
115+ fail("超过 maxTotal 且超过 maxWaitMillis 应抛出 SQLException");
116+ } catch (SQLException e) {
117+ long cost = System.currentTimeMillis() - begin;
118+ assertTrue("等待时长应接近 maxWaitMillis(2000ms),实际: " + cost + "ms", cost >= 1500);
119+ assertTrue("等待时长不应远超 maxWaitMillis(2000ms),实际: " + cost + "ms", cost <= 5000);
120+ assertTrue("应给出明确的池耗尽异常,实际: " + e.getMessage(),
121+ e.getMessage() != null
122+ && e.getMessage().contains("Timeout waiting for idle object"));
123+ assertTrue("池耗尽异常应链式包含 NoSuchElementException,实际 cause: "
124+ + (e.getCause() == null ? null : e.getCause()),
125+ e.getCause() instanceof NoSuchElementException
126+ && e.getCause().getMessage() != null
127+ && e.getCause().getMessage().contains("Timeout waiting for idle object"));
128+ }
129+ }
130+ } finally {
131+ closeConnectionQuietly(c2);
132+ closeDataSourceQuietly(ds);
133+ }
134+ }
135+ 
136+ /**
137+ * 满池真实业务行为:8 条连接分别在事务中完成 CRUD 后继续持有;
138+ * 第 9 条请求先在无释放时超时,再验证释放任一连接后的恢复执行。
139+ */
140+ @Test
141+ public void testMaxTotalConcurrentCrudAndQueuedBorrow() throws Exception {
142+ final int maxTotal = 8;
143+ final long waitMillis = 5000;
144+ final long workerHoldMillis = 15000;
145+ BasicDataSource ds = newPool();
146+ ds.setInitialSize(0);
147+ ds.setMinIdle(0);
148+ ds.setMaxTotal(maxTotal);
149+ ds.setMaxWaitMillis(waitMillis);
150+ 
151+ ExecutorService pool = Executors.newFixedThreadPool(maxTotal + 1);
152+ CountDownLatch workersHoldingConnections = new CountDownLatch(maxTotal);
153+ CountDownLatch releaseWorkers = new CountDownLatch(1);
154+ List<Future<Boolean>> workers = new ArrayList<>();
155+ String runId = "pool_max_" + System.nanoTime();
156+ try {
157+ for (int i = 0; i < maxTotal; i++) {
158+ final String name = runId + "_" + i;
159+ workers.add(pool.submit(() -> {
160+ boolean completedCrud = false;
161+ try (Connection conn = ds.getConnection()) {
162+ executeCrudInTransaction(conn, name, 20);
163+ completedCrud = true;
164+ workersHoldingConnections.countDown();
165+ return releaseWorkers.await(workerHoldMillis, TimeUnit.MILLISECONDS);
166+ } finally {
167+ // 失败路径同样唤醒主线程,后续 Future.get 会保留原始失败原因。
168+ if (!completedCrud) {
169+ workersHoldingConnections.countDown();
170+ }
171+ }
172+ }));
173+ }
174+ 
175+ assertTrue("所有 worker 应在 20s 内完成事务并保持借出连接",
176+ workersHoldingConnections.await(20, TimeUnit.SECONDS));
177+ assertEquals("8 条连接均应处于 active,不能只验证建连成功",
178+ maxTotal, ds.getNumActive());
179+ 
180+ Future<Boolean> timedOutOverflow = pool.submit(() -> {
181+ try (Connection ignored = ds.getConnection()) {
182+ return false;
183+ } catch (SQLException e) {
184+ return e.getMessage() != null
185+ && e.getMessage().contains("Timeout waiting for idle object");
186+ }
187+ });
188+ assertTrue("8 条连接均不释放时,第 9 条请求应在 maxWaitMillis 后超时",
189+ timedOutOverflow.get(waitMillis + 3000, TimeUnit.MILLISECONDS));
190+ assertEquals("第 9 条请求超时后,8 条 worker 仍应处于 active",
191+ maxTotal, ds.getNumActive());
192+ 
193+ CountDownLatch overflowRequested = new CountDownLatch(1);
194+ long overflowBegin = System.nanoTime();
195+ Future<Boolean> overflow = pool.submit(() -> {
196+ overflowRequested.countDown();
197+ try (Connection conn = ds.getConnection();
198+ Statement st = conn.createStatement();
199+ ResultSet rs = st.executeQuery("SELECT 1")) {
200+ return rs.next() && rs.getInt(1) == 1;
201+ }
202+ });
203+ 
204+ assertTrue("第 9 条连接请求未发起", overflowRequested.await(2, TimeUnit.SECONDS));
205+ Thread.sleep(300);
206+ assertFalse("达到 maxTotal 后,第 9 条请求应等待空闲连接而不能立即成功", overflow.isDone());
207+ assertEquals("排队期间 active 不得超过 maxTotal", maxTotal, ds.getNumActive());
208+ 
209+ releaseWorkers.countDown();
210+ for (Future<Boolean> worker : workers) {
211+ assertTrue("worker 未在满池释放后正常结束", worker.get(10, TimeUnit.SECONDS));
212+ }
213+ assertTrue("释放连接后,第 9 条请求应恢复并执行 SQL", overflow.get(10, TimeUnit.SECONDS));
214+ long overflowCost = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - overflowBegin);
215+ assertTrue("第 9 条请求应实际经历排队等待,耗时: " + overflowCost + "ms",
216+ overflowCost >= 250);
217+ assertEquals("全部归还后不应遗留 active 连接", 0, ds.getNumActive());
218+ } finally {
219+ releaseWorkers.countDown();
220+ shutdownExecutorAndClosePool(pool, ds, 15000, "满池并发 CRUD 线程未在超时内终止");
221+ }
222+ }
223+ 
224+ /**
225+ * maxIdle:并发创建 maxTotal 个物理连接,全部归还后空闲连接数应精确收敛到 maxIdle。
226+ * 采用 held/release 两阶段屏障:工作线程内部 try-with-resources 自行归还,
227+ * 主线程确认 8 个连接同时 active 后统一放行;Future/await 全部带超时,
228+ * 任何失败路径都由 finally 停止线程池并关闭 ds,不会丢失借出的连接。
229+ */
230+ @Test
231+ public void testMaxIdleLimitsIdleConnections() throws Exception {
232+ int connCount = 8;
233+ int maxIdle = 2;
234+ BasicDataSource ds = newPool();
235+ ds.setMaxTotal(connCount);
236+ ds.setMaxIdle(maxIdle);
237+ ds.setMaxWaitMillis(10000);
238+ ExecutorService pool = Executors.newFixedThreadPool(connCount);
239+ try {
240+ CountDownLatch held = new CountDownLatch(connCount); // 阶段1:全部线程已借到连接
241+ CountDownLatch release = new CountDownLatch(1); // 阶段2:放行归还
242+ CountDownLatch done = new CountDownLatch(connCount);
243+ List<Future<Boolean>> results = new ArrayList<>();
244+ for (int i = 0; i < connCount; i++) {
245+ results.add(pool.submit(() -> {
246+ try (Connection c = ds.getConnection()) {
247+ held.countDown();
248+ if (!release.await(15, TimeUnit.SECONDS)) {
249+ return false;
250+ }
251+ return true;
252+ } finally {
253+ done.countDown();
254+ }
255+ }));
256+ }
257+ assertTrue("8 个线程应在 15s 内全部借到连接", held.await(15, TimeUnit.SECONDS));
258+ assertEquals("8 个连接应同时 active(全部持有)", connCount, ds.getNumActive());
259+ 
260+ release.countDown();
261+ assertTrue("8 个线程应在 15s 内全部归还", done.await(15, TimeUnit.SECONDS));
262+ for (Future<Boolean> f : results) {
263+ assertTrue("借出/归还执行失败", f.get(10, TimeUnit.SECONDS));
264+ }
265+ 
266+ // 全部归还后:active 必须为 0(无泄漏),空闲数必须精确收敛到 maxIdle=2
267+ waitForCondition(() -> ds.getNumIdle() == maxIdle, 10000,
268+ "全部归还后空闲连接数应精确收敛到 maxIdle=" + maxIdle);
269+ assertEquals("全部归还后 active 应为 0", 0, ds.getNumActive());
270+ assertEquals("归还后空闲连接数应恰好等于 maxIdle,实际: " + ds.getNumIdle(),
271+ maxIdle, ds.getNumIdle());
272+ } finally {
273+ shutdownExecutorAndClosePool(pool, ds, 15000, "maxIdle 借出线程未在超时内终止");
274+ }
275+ }
276+ 
277+ /**
278+ * minIdle:启动维护线程,轮询断言空闲连接被补足到 minIdle;
279+ * 且在被借走期间维护线程仍继续补足。
280+ */
281+ @Test
282+ public void testMinIdleReplenishedByMaintenanceThread() throws Exception {
283+ int minIdle = 2;
284+ BasicDataSource ds = newPool();
285+ ds.setInitialSize(0);
286+ ds.setMinIdle(minIdle);
287+ ds.setMaxIdle(5);
288+ ds.setMaxTotal(5);
289+ ds.setTimeBetweenEvictionRunsMillis(300);
290+ Connection a = null;
291+ Connection b = null;
292+ try {
293+ try (Connection c = ds.getConnection()) {
294+ // 首次借出触发建池
295+ }
296+ waitForCondition(() -> ds.getNumIdle() >= minIdle, 15000,
297+ "维护线程应在池建成后把空闲连接补足到 minIdle=" + minIdle);
298+ assertEquals("空闲补足期间不应有借出的连接", 0, ds.getNumActive());
299+ assertTrue("minIdle 补足后空闲连接数应 >= " + minIdle + ",实际: " + ds.getNumIdle(),
300+ ds.getNumIdle() >= minIdle);
301+ 
302+ // 借走全部空闲连接后,维护线程仍应把空闲补足回 minIdle(active 保持被借出状态)
303+ a = ds.getConnection();
304+ b = ds.getConnection();
305+ assertEquals("借走 2 个空闲连接后 idle 应为 0", 0, ds.getNumIdle());
306+ waitForCondition(() -> ds.getNumIdle() >= minIdle, 15000,
307+ "持有连接期间维护线程仍应补足空闲连接到 minIdle");
308+ assertEquals("补足期间被借出的连接不应被释放", 2, ds.getNumActive());
309+ } finally {
310+ if (a != null) {
311+ try {
312+ a.close();
313+ } catch (SQLException ignored) {
314+ }
315+ }
316+ if (b != null) {
317+ try {
318+ b.close();
319+ } catch (SQLException ignored) {
320+ }
321+ }
322+ closeDataSourceQuietly(ds);
323+ }
324+ }
325+ 
326+ /**
327+ * 连接归还复用:maxTotal=1,close 归还后重新借出应复用同一物理连接(同一会话、同一对象)。
328+ */
329+ @Test
330+ public void testReturnAndReborrowReusesPhysicalSession() throws Exception {
331+ BasicDataSource ds = newPool();
332+ ds.setMaxTotal(1);
333+ ds.setMaxWaitMillis(3000);
334+ try {
335+ com.mysql.cj.jdbc.ConnectionImpl phys1;
336+ long id1;
337+ try (Connection c = ds.getConnection()) {
338+ phys1 = physicalConnection(c);
339+ id1 = connectionId(c);
340+ }
341+ com.mysql.cj.jdbc.ConnectionImpl phys2;
342+ long id2;
343+ try (Connection c = ds.getConnection()) {
344+ phys2 = physicalConnection(c);
345+ id2 = connectionId(c);
346+ }
347+ assertTrue("close 归还后重新借出应复用同一物理连接对象", phys1 == phys2);
348+ assertEquals("close 归还后重新借出应复用同一服务端会话(CONNECTION_ID 相同)", id1, id2);
349+ } finally {
350+ closeDataSourceQuietly(ds);
351+ }
352+ }
353+ 
354+ /**
355+ * 连接状态复位:归还前修改 autoCommit/isolation,重新借出后应恢复默认值。
356+ * 注:dolphin 的 MySQL 协议实测不支持 readOnly(setReadOnly(true) 后 isReadOnly()
357+ * 仍为 false),故本用例只断言 autoCommit 与隔离级别的复位,不声明 readOnly 复位。
358+ */
359+ @Test
360+ public void testStateResetOnReborrow() throws Exception {
361+ BasicDataSource ds = newPool();
362+ ds.setMaxTotal(1);
363+ ds.setMaxWaitMillis(3000);
364+ ds.setDefaultAutoCommit(true);
365+ ds.setDefaultTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
366+ 
367+ Connection c1 = null;
368+ try {
369+ c1 = ds.getConnection();
370+ c1.setAutoCommit(false);
371+ c1.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
372+ assertEquals("归还前 autoCommit 应已被修改为 false", false, c1.getAutoCommit());
373+ assertEquals("归还前隔离级别应已被修改为 READ_COMMITTED",
374+ Connection.TRANSACTION_READ_COMMITTED, c1.getTransactionIsolation());
375+ c1.close();
376+ c1 = null;
377+ 
378+ try (Connection c2 = ds.getConnection()) {
379+ assertEquals("重新借出后 autoCommit 应复位为默认 true", true, c2.getAutoCommit());
380+ assertEquals("重新借出后隔离级别应复位为默认 REPEATABLE_READ",
381+ Connection.TRANSACTION_REPEATABLE_READ, c2.getTransactionIsolation());
382+ }
383+ } finally {
384+ if (c1 != null) {
385+ try {
386+ c1.close();
387+ } catch (SQLException ignored) {
388+ }
389+ }
390+ closeDataSourceQuietly(ds);
391+ }
392+ }
393+ 
394+ /**
395+ * 连接泄漏回收:removeAbandoned 通过维护线程在等待期间周期回收废弃连接。
396+ * leak/second/third 均在 try 内获取,finally 逐个 quiet close(单个失败不阻断其余清理),
397+ * ds 使用 closeDataSourceQuietly;等待上界按 maxWaitMillis + 合理调度容差断言。
398+ */
399+ @Test
400+ public void testLeakReclamationWithRemoveAbandoned() throws Exception {
401+ BasicDataSource ds = newPool();
402+ ds.setMaxTotal(2);
403+ ds.setMaxWaitMillis(8000);
404+ ds.setRemoveAbandonedOnBorrow(true);
405+ ds.setRemoveAbandonedTimeout(5); // 借出超过 5 秒未使用视为泄漏
406+ ds.setRemoveAbandonedOnMaintenance(true);
407+ ds.setTimeBetweenEvictionRunsMillis(1000);
408+ Connection leak = null; // 故意不归还(模拟泄漏)
409+ Connection second = null;
410+ Connection third = null;
411+ try {
412+ leak = ds.getConnection();
413+ second = ds.getConnection();
414+ long begin = System.currentTimeMillis();
415+ try {
416+ third = ds.getConnection();
417+ } catch (SQLException e) {
418+ fail("等待 removeAbandonedTimeout 后应能借出第三个连接: " + e.getMessage());
419+ }
420+ assertFalse("回收后借出的连接应可用", third.isClosed());
421+ long cost = System.currentTimeMillis() - begin;
422+ assertTrue("回收应等待 removeAbandonedTimeout(5s) 而非立即成功,实际: " + cost + "ms",
423+ cost >= 4000);
424+ assertTrue("回收等待不应超过 maxWaitMillis(8000ms) + 调度容差,实际: " + cost + "ms",
425+ cost <= 8000 + 3000);
426+ } finally {
427+ closeConnectionQuietly(third);
428+ closeConnectionQuietly(second);
429+ closeConnectionQuietly(leak); // 已被回收,close 幂等
430+ closeDataSourceQuietly(ds);
431+ }
432+ }
433+ 
434+ /**
435+ * 空闲驱逐:空闲连接超过 minEvictableIdleTimeMillis 后被维护线程驱逐到 minIdle。
436+ */
437+ @Test
438+ public void testIdleEvictionByMinEvictableIdleTime() throws Exception {
439+ BasicDataSource ds = newPool();
440+ ds.setInitialSize(2);
441+ ds.setMinIdle(0);
442+ ds.setMinEvictableIdleTimeMillis(3000);
443+ ds.setTimeBetweenEvictionRunsMillis(1000);
444+ try {
445+ try (Connection c = ds.getConnection()) {
446+ // 首次借出触发建池,归还后 2 个空闲
447+ }
448+ waitForCondition(() -> ds.getNumIdle() < 2, 15000,
449+ "空闲连接应在超过 minEvictableIdleTimeMillis 后被驱逐到 minIdle=0");
450+ assertEquals(0, ds.getNumActive());
451+ } finally {
452+ closeDataSourceQuietly(ds);
453+ }
454+ }
455+ 
456+ private static void executeCrudInTransaction(Connection conn, String name, int initialAge) throws SQLException {
457+ conn.setAutoCommit(false);
458+ try {
459+ try (PreparedStatement ps = conn.prepareStatement(
460+ "INSERT INTO `user`(`name`,`age`) VALUES (?,?)")) {
461+ ps.setString(1, name);
462+ ps.setInt(2, initialAge);
463+ assertEquals("事务内 INSERT 应影响一行", 1, ps.executeUpdate());
464+ }
465+ try (PreparedStatement ps = conn.prepareStatement(
466+ "SELECT `age` FROM `user` WHERE `name`=?")) {
467+ ps.setString(1, name);
468+ try (ResultSet rs = ps.executeQuery()) {
469+ assertTrue("事务内 INSERT 后应可查询到数据", rs.next());
470+ assertEquals(initialAge, rs.getInt(1));
471+ }
472+ }
473+ try (PreparedStatement ps = conn.prepareStatement(
474+ "UPDATE `user` SET `age`=? WHERE `name`=?")) {
475+ ps.setInt(1, initialAge + 1);
476+ ps.setString(2, name);
477+ assertEquals("事务内 UPDATE 应影响一行", 1, ps.executeUpdate());
478+ }
479+ try (PreparedStatement ps = conn.prepareStatement(
480+ "SELECT `age` FROM `user` WHERE `name`=?")) {
481+ ps.setString(1, name);
482+ try (ResultSet rs = ps.executeQuery()) {
483+ assertTrue("事务内 UPDATE 后应可查询到数据", rs.next());
484+ assertEquals(initialAge + 1, rs.getInt(1));
485+ }
486+ }
487+ try (PreparedStatement ps = conn.prepareStatement(
488+ "DELETE FROM `user` WHERE `name`=?")) {
489+ ps.setString(1, name);
490+ assertEquals("事务内 DELETE 应影响一行", 1, ps.executeUpdate());
491+ }
492+ conn.commit();
493+ } catch (SQLException | RuntimeException e) {
494+ conn.rollback();
495+ throw e;
496+ }
497+ }
498+}
@@ -0,0 +1,156 @@
1+package org.opengauss.test;
2+ 
3+import org.apache.commons.dbcp2.BasicDataSource;
4+import org.junit.Test;
5+ 
6+import java.sql.Connection;
7+import java.sql.ResultSet;
8+import java.sql.SQLException;
9+import java.sql.Statement;
10+ 
11+import static org.junit.Assert.assertEquals;
12+import static org.junit.Assert.assertFalse;
13+import static org.junit.Assert.assertTrue;
14+ 
15+/**
16+ * 连接校验与自愈测试:
17+ * testOnBorrow 校验、testWhileIdle 后台校验、连接池对失效连接(服务端断连)的自愈能力。
18+ * 服务端断连通过**独立管理连接执行 KILL &lt;CONNECTION_ID&gt;** 实现(不再调用被测
19+ * ConnectionImpl.close()),并先用 SHOW PROCESSLIST 复核服务端会话确实已失效,
20+ * 再验证 testOnBorrow / testWhileIdle 丢弃死连接并重建。
21+ * 均显式 testOnReturn=false,避免归还路径的校验干扰对“死连接已进入空闲池”的判定。
22+ */
23+public class ConnectionValidationTest extends BaseDbTest {
24+ 
25+ @Test
26+ public void testValidationQueryOnBorrow() throws Exception {
27+ // 基类连接池 testOnBorrow=true:借出即执行 SELECT 1 校验
28+ try (Connection conn = dataSource.getConnection()) {
29+ assertFalse(conn.isClosed());
30+ assertTrue("testOnBorrow=true 时借出的连接应通过校验并可用", conn.isValid(3));
31+ }
32+ }
33+ 
34+ /**
35+ * 服务端断连后,testOnBorrow 应丢弃死连接并重建新物理连接。
36+ * 流程:KILL 会话 → SHOW PROCESSLIST 确认消失 → 本地 isClosed() 仍为 false
37+ * 逻辑归还(testOnReturn=false)后 idle==1 证明死连接进入空闲池 → 再次借出时
38+ * testOnBorrow 校验失败才丢弃并重建。
39+ */
40+ @Test
41+ public void testOnBorrowDiscardsKilledConnectionAndRebuilds() throws Exception {
42+ BasicDataSource ds = newPool();
43+ ds.setMaxTotal(1);
44+ ds.setMaxWaitMillis(5000);
45+ ds.setTestOnBorrow(true);
46+ ds.setTestWhileIdle(false);
47+ ds.setTestOnReturn(false);
48+ ds.setValidationQuery("SELECT 1");
49+ 
50+ Connection c1 = null;
51+ try {
52+ c1 = ds.getConnection();
53+ com.mysql.cj.jdbc.ConnectionImpl phys1 = physicalConnection(c1);
54+ long killedId = connectionId(c1);
55+ // 服务端断连:独立管理连接执行 KILL,不调用被测 ConnectionImpl.close()
56+ killServerSession(killedId);
57+ // 复核:服务端会话确实消失(而非仅客户端标记关闭)
58+ waitForCondition(() -> {
59+ try {
60+ return !serverSessionExists(killedId);
61+ } catch (SQLException e) {
62+ return false; // 查询失败视作会话尚未消失,继续轮询
63+ }
64+ }, 5000, "KILL 后服务端会话应消失,id=" + killedId);
65+ // 本地视图:被测 JDBC/物理连接仍报告未关闭,证明是服务端终止而非客户端 close
66+ assertFalse("服务端 KILL 后本地 isClosed() 仍应为 false", c1.isClosed());
67+ 
68+ c1.close(); // 逻辑归还(testOnReturn=false,归还不做校验)
69+ c1 = null;
70+ assertEquals("死连接应进入空闲池(testOnReturn=false 归还未校验)", 1, ds.getNumIdle());
71+ 
72+ // 再次借出:testOnBorrow 校验失败 → 丢弃死连接 → 新建可用连接(自愈)
73+ try (Connection c2 = ds.getConnection();
74+ Statement st = c2.createStatement();
75+ ResultSet rs = st.executeQuery("SELECT 1")) {
76+ assertTrue("重建后的连接应可执行查询", rs.next() && rs.getInt(1) == 1);
77+ assertFalse("池应自愈并提供可用连接", c2.isClosed());
78+ com.mysql.cj.jdbc.ConnectionImpl phys2 = physicalConnection(c2);
79+ assertTrue("testOnBorrow 应丢弃死连接并创建新物理连接(对象应不同)",
80+ phys2 != phys1);
81+ }
82+ } finally {
83+ if (c1 != null) {
84+ try {
85+ c1.close();
86+ } catch (SQLException ignored) {
87+ }
88+ }
89+ closeDataSourceQuietly(ds);
90+ }
91+ }
92+ 
93+ /**
94+ * 服务端断连后,testWhileIdle 应通过后台校验清除死连接。
95+ * 流程:KILL 会话 → SHOW PROCESSLIST 确认消失 → 本地 isClosed() 仍为 false
96+ * 逻辑归还(testOnReturn=false)后、维护线程首轮前 idle==1 → 后台校验轮询到 idle==0
97+ * → 再次借出得到新物理对象。
98+ */
99+ @Test
100+ public void testWhileIdleEvictsKilledConnection() throws Exception {
101+ BasicDataSource ds = newPool();
102+ ds.setMaxTotal(2);
103+ ds.setMaxIdle(1);
104+ ds.setMinIdle(0);
105+ ds.setInitialSize(1);
106+ ds.setTestOnBorrow(false);
107+ ds.setTestWhileIdle(true);
108+ ds.setTestOnReturn(false);
109+ ds.setValidationQuery("SELECT 1");
110+ // 首轮维护跑批在 10s 后,保证有足够时间完成“KILL→归还→断言 idle==1”
111+ ds.setTimeBetweenEvictionRunsMillis(10000);
112+ // 调大空闲时长阈值:确保只有 testWhileIdle 的校验路径会移除死连接(而非按空闲时长驱逐)
113+ ds.setMinEvictableIdleTimeMillis(60000);
114+ 
115+ Connection c1 = null;
116+ try {
117+ c1 = ds.getConnection();
118+ com.mysql.cj.jdbc.ConnectionImpl phys1 = physicalConnection(c1);
119+ long killedId = connectionId(c1);
120+ killServerSession(killedId);
121+ waitForCondition(() -> {
122+ try {
123+ return !serverSessionExists(killedId);
124+ } catch (SQLException e) {
125+ return false;
126+ }
127+ }, 5000, "KILL 后服务端会话应消失,id=" + killedId);
128+ assertFalse("服务端 KILL 后本地 isClosed() 仍应为 false", c1.isClosed());
129+ 
130+ c1.close(); // 逻辑归还(testOnReturn=false)
131+ c1 = null;
132+ // 维护线程首轮(10s)之前,死连接应已在空闲池中
133+ assertEquals("维护线程首轮前死连接应进入空闲池(idle==1)", 1, ds.getNumIdle());
134+ 
135+ // 后台维护线程通过 testWhileIdle 校验清除死连接:空闲数应回到 0
136+ waitForCondition(() -> ds.getNumIdle() == 0, 15000,
137+ "testWhileIdle 应清除死连接(minIdle=0 且不会按时间驱逐)");
138+ 
139+ // 再次借出应得到全新的物理会话
140+ try (Connection c2 = ds.getConnection()) {
141+ assertFalse("重新借出的连接应可用", c2.isClosed());
142+ com.mysql.cj.jdbc.ConnectionImpl phys2 = physicalConnection(c2);
143+ assertTrue("testWhileIdle 清除死连接后应创建新物理连接(对象应不同)",
144+ phys2 != phys1);
145+ }
146+ } finally {
147+ if (c1 != null) {
148+ try {
149+ c1.close();
150+ } catch (SQLException ignored) {
151+ }
152+ }
153+ closeDataSourceQuietly(ds);
154+ }
155+ }
156+}
@@ -0,0 +1,85 @@
1+package org.opengauss.test;
2+ 
3+import org.opengauss.test.util.TestConfig;
4+import org.apache.commons.dbcp2.BasicDataSource;
5+import org.junit.Test;
6+ 
7+import java.sql.Connection;
8+import java.sql.ResultSet;
9+import java.sql.SQLException;
10+import java.sql.Statement;
11+import java.util.Properties;
12+ 
13+import static org.junit.Assert.assertEquals;
14+import static org.junit.Assert.assertTrue;
15+import static org.junit.Assert.fail;
16+ 
17+/**
18+ * JDBC 连接参数和受限行为验证:
19+ * <ul>
20+ * <li>allowMultiQueries 默认 off:分号多查询必须报含 "Multi-statement is not allowed" 的明确错误。</li>
21+ * <li>useCursorFetch=true + setFetchSize(10):仅验证该配置下查询可执行并精确返回 3 行;
22+ * <b>不证明使用了服务端游标,也不证明 dolphin 支持/不支持服务端游标</b>。</li>
23+ * <li>useSSL=false:验证本地未启用 SSL 的 Dolphin MySQL 协议端口可以连接。</li>
24+ * </ul>
25+ */
26+public class ConstraintTest extends BaseDbTest {
27+ 
28+ /** 约束:allowMultiQueries 默认 off —— 分号分隔多查询必须报明确的 "Multi-statement is not allowed" */
29+ @Test
30+ public void testAllowMultiQueriesDefaultOff() throws Exception {
31+ try (Connection conn = dataSource.getConnection();
32+ Statement st = conn.createStatement()) {
33+ try {
34+ st.execute("SELECT 1; SELECT 2");
35+ fail("allowMultiQueries 默认关闭,分号分隔多查询应报错");
36+ } catch (SQLException e) {
37+ assertTrue("应给出明确的 Multi-statement 报错,实际: " + e.getMessage(),
38+ e.getMessage() != null && e.getMessage().contains("Multi-statement is not allowed"));
39+ }
40+ }
41+ }
42+ 
43+ /**
44+ * 配置兼容性验证:useCursorFetch=true + setFetchSize(10) 下查询应可执行并精确返回 3 行。
45+ * 注意:这只是配置兼容性 / 查询可用性验证,<b>不证明使用了服务端游标</b>,
46+ * <b>也不证明 dolphin 支持或不支持服务端游标</b>。
47+ */
48+ @Test
49+ public void testCursorFetchConfigCompatibility() throws Exception {
50+ Properties p = TestConfig.load();
51+ String baseUrl = p.getProperty("url");
52+ String cursorUrl = baseUrl + (baseUrl.contains("?") ? "&" : "?")
53+ + "useCursorFetch=true&defaultFetchSize=10";
54+ BasicDataSource ds = new BasicDataSource();
55+ ds.setDriverClassName(p.getProperty("driverClassName"));
56+ ds.setUrl(cursorUrl);
57+ ds.setUsername(p.getProperty("username"));
58+ ds.setPassword(p.getProperty("password"));
59+ ds.setMaxTotal(1);
60+ ds.setMaxWaitMillis(5000);
61+ try (Connection conn = ds.getConnection();
62+ Statement st = conn.createStatement()) {
63+ st.setFetchSize(10);
64+ try (ResultSet rs = st.executeQuery("SELECT id,name FROM `user` ORDER BY id LIMIT 3")) {
65+ int n = 0;
66+ while (rs.next()) {
67+ n++;
68+ }
69+ assertEquals("useCursorFetch=true 配置下查询应可执行并精确返回 3 行", 3, n);
70+ }
71+ } finally {
72+ closeDataSourceQuietly(ds);
73+ }
74+ }
75+ 
76+ /** 使用 useSSL=false 的 URL 可正常连接(其余用例均基于此配置) */
77+ @Test
78+ public void testUseSslFalseConnectionWorks() throws Exception {
79+ String url = TestConfig.load().getProperty("url");
80+ assertTrue("连接串应显式包含 useSSL=false(文档要求)", url.contains("useSSL=false"));
81+ try (Connection conn = dataSource.getConnection()) {
82+ assertTrue(conn.isValid(3));
83+ }
84+ }
85+}
@@ -0,0 +1,101 @@
1+package org.opengauss.test;
2+ 
3+import org.opengauss.dao.UserDao;
4+import org.opengauss.entity.User;
5+import org.junit.Before;
6+import org.junit.Test;
7+ 
8+import java.util.Arrays;
9+import java.util.List;
10+ 
11+import static org.junit.Assert.assertEquals;
12+import static org.junit.Assert.assertNotNull;
13+import static org.junit.Assert.assertNull;
14+import static org.junit.Assert.assertTrue;
15+ 
16+/**
17+ * CRUD 功能测试:增删改查、自增主键回填、模糊查询、分页、批量
18+ * 依赖 mysql_test_db 库中的 user 表(见 src/main/resources/init.sql)
19+ */
20+public class CrudTest extends BaseDbTest {
21+ 
22+ private UserDao userDao;
23+ 
24+ @Before
25+ public void setUp() {
26+ userDao = new UserDao(dataSource);
27+ }
28+ 
29+ @Test
30+ public void testInsertBackfillAutoIncrementKey() throws Exception {
31+ User u = new User("crud_测试_" + System.currentTimeMillis(), 18);
32+ assertEquals(1, userDao.insert(u));
33+ assertNotNull("自增主键应回填", u.getId());
34+ User db = userDao.findById(u.getId());
35+ assertNotNull(db);
36+ assertEquals(u.getName(), db.getName());
37+ assertEquals(u.getAge(), db.getAge());
38+ userDao.delete(u.getId());
39+ }
40+ 
41+ @Test
42+ public void testFindAll() throws Exception {
43+ List<User> all = userDao.findAll();
44+ assertTrue("初始化数据应至少有 3 行,实际: " + all.size(), all.size() >= 3);
45+ }
46+ 
47+ @Test
48+ public void testFindByIdNotExistReturnsNull() throws Exception {
49+ assertNull(userDao.findById(-1));
50+ }
51+ 
52+ @Test
53+ public void testFindByNameLike() throws Exception {
54+ List<User> like = userDao.findByNameLike("张");
55+ assertTrue("应能模糊查到一个以上用户,实际: " + like.size(), like.size() >= 1);
56+ like.forEach(u -> assertTrue(u.getName().contains("张")));
57+ }
58+ 
59+ @Test
60+ public void testFindByPage() throws Exception {
61+ List<User> page = userDao.findByPage(2, 0);
62+ assertEquals("LIMIT 2 应返回 2 行", 2, page.size());
63+ }
64+ 
65+ @Test
66+ public void testUpdate() throws Exception {
67+ User u = new User("crud_update_" + System.currentTimeMillis(), 20);
68+ userDao.insert(u);
69+ u.setAge(30);
70+ assertEquals(1, userDao.update(u));
71+ User db = userDao.findById(u.getId());
72+ assertEquals(Integer.valueOf(30), db.getAge());
73+ userDao.delete(u.getId());
74+ }
75+ 
76+ @Test
77+ public void testDelete() throws Exception {
78+ User u = new User("crud_del_" + System.currentTimeMillis(), 20);
79+ userDao.insert(u);
80+ assertEquals(1, userDao.delete(u.getId()));
81+ assertNull(userDao.findById(u.getId()));
82+ }
83+ 
84+ @Test
85+ public void testBatchInsert() throws Exception {
86+ List<User> users = Arrays.asList(
87+ new User("批_一号", 21),
88+ new User("批_二号", 22),
89+ new User("批_三号", 23));
90+ int[] rows = userDao.batchInsert(users);
91+ assertEquals("批量插入应返回每行影响行数", 3, rows.length);
92+ users.forEach(u -> assertNotNull("批量插入应回填主键", u.getId()));
93+ users.forEach(u -> {
94+ try {
95+ userDao.delete(u.getId());
96+ } catch (Exception e) {
97+ throw new RuntimeException(e);
98+ }
99+ });
100+ }
101+}
@@ -0,0 +1,119 @@
1+package org.opengauss.test;
2+ 
3+import org.junit.After;
4+import org.junit.Before;
5+import org.junit.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.Statement;
13+import java.sql.Timestamp;
14+import java.sql.Types;
15+ 
16+import static org.junit.Assert.assertEquals;
17+import static org.junit.Assert.assertNotNull;
18+import static org.junit.Assert.assertTrue;
19+ 
20+/**
21+ * 数据类型与字符集测试:中文/emoji(utf8mb4)、常用类型往返读写、NULL 处理。
22+ * 用例自建测试表,互不依赖,执行完自动清理。
23+ */
24+public class DataTypeCharsetTest extends BaseDbTest {
25+ 
26+ @Before
27+ public void createTable() throws Exception {
28+ try (Connection conn = dataSource.getConnection();
29+ Statement st = conn.createStatement()) {
30+ st.execute("DROP TABLE IF EXISTS test_datatype");
31+ st.execute("CREATE TABLE test_datatype ("
32+ + "id INT AUTO_INCREMENT PRIMARY KEY, "
33+ + "c_varchar VARCHAR(100), "
34+ + "c_int INT, "
35+ + "c_bigint BIGINT, "
36+ + "c_decimal DECIMAL(10,2), "
37+ + "c_date DATE, "
38+ + "c_datetime DATETIME, "
39+ + "c_text TEXT, "
40+ + "c_nullable VARCHAR(20))");
41+ }
42+ }
43+ 
44+ @After
45+ public void dropTable() throws Exception {
46+ try (Connection conn = dataSource.getConnection();
47+ Statement st = conn.createStatement()) {
48+ st.execute("DROP TABLE IF EXISTS test_datatype");
49+ }
50+ }
51+ 
52+ @Test
53+ public void testChineseAndEmojiRoundTrip() throws Exception {
54+ String value = "中文😀emoji测试_" + System.currentTimeMillis();
55+ try (Connection conn = dataSource.getConnection();
56+ PreparedStatement ps = conn.prepareStatement(
57+ "INSERT INTO test_datatype(c_varchar) VALUES (?)")) {
58+ ps.setString(1, value);
59+ ps.executeUpdate();
60+ }
61+ try (Connection conn = dataSource.getConnection();
62+ PreparedStatement ps = conn.prepareStatement(
63+ "SELECT c_varchar FROM test_datatype WHERE c_varchar=?")) {
64+ ps.setString(1, value);
65+ try (ResultSet rs = ps.executeQuery()) {
66+ assertTrue("插入的数据应能查到", rs.next());
67+ assertEquals("中文与 emoji 应无损读写", value, rs.getString(1));
68+ }
69+ }
70+ }
71+ 
72+ @Test
73+ public void testCommonTypesRoundTrip() throws Exception {
74+ long bigValue = 9223372036854775807L;
75+ try (Connection conn = dataSource.getConnection();
76+ PreparedStatement ps = conn.prepareStatement(
77+ "INSERT INTO test_datatype(c_int,c_bigint,c_decimal,c_date,c_datetime,c_text) "
78+ + "VALUES (?,?,?,?,?,?)")) {
79+ ps.setInt(1, 123);
80+ ps.setLong(2, bigValue);
81+ ps.setBigDecimal(3, new BigDecimal("12345.67"));
82+ ps.setDate(4, Date.valueOf("2026-08-06"));
83+ ps.setTimestamp(5, Timestamp.valueOf("2026-08-06 12:30:00"));
84+ ps.setString(6, "TEXT内容");
85+ ps.executeUpdate();
86+ }
87+ try (Connection conn = dataSource.getConnection();
88+ PreparedStatement ps = conn.prepareStatement(
89+ "SELECT c_int,c_bigint,c_decimal,c_date,c_datetime,c_text FROM test_datatype WHERE c_int=123");
90+ ResultSet rs = ps.executeQuery()) {
91+ assertTrue(rs.next());
92+ assertEquals(123, rs.getInt(1));
93+ assertEquals(bigValue, rs.getLong(2));
94+ assertEquals(0, new BigDecimal("12345.67").compareTo(rs.getBigDecimal(3)));
95+ assertEquals(Date.valueOf("2026-08-06"), rs.getDate(4));
96+ assertNotNull("DATETIME 应正常读写", rs.getTimestamp(5));
97+ assertEquals("TEXT内容", rs.getString(6));
98+ }
99+ }
100+ 
101+ @Test
102+ public void testNullHandling() throws Exception {
103+ try (Connection conn = dataSource.getConnection();
104+ PreparedStatement ps = conn.prepareStatement(
105+ "INSERT INTO test_datatype(c_varchar, c_nullable) VALUES (?, ?)")) {
106+ ps.setString(1, "null_test");
107+ ps.setNull(2, Types.VARCHAR);
108+ ps.executeUpdate();
109+ }
110+ try (Connection conn = dataSource.getConnection();
111+ PreparedStatement ps = conn.prepareStatement(
112+ "SELECT c_nullable FROM test_datatype WHERE c_varchar='null_test'");
113+ ResultSet rs = ps.executeQuery()) {
114+ assertTrue(rs.next());
115+ rs.getString(1);
116+ assertTrue("NULL 应通过 wasNull() 识别", rs.wasNull());
117+ }
118+ }
119+}
@@ -0,0 +1,104 @@
1+package org.opengauss.test;
2+ 
3+import org.apache.commons.dbcp2.BasicDataSource;
4+import org.opengauss.test.util.TestConfig;
5+import org.junit.Test;
6+ 
7+import java.sql.Connection;
8+import java.sql.SQLException;
9+import java.util.Properties;
10+ 
11+import static org.junit.Assert.assertEquals;
12+import static org.junit.Assert.assertNotNull;
13+import static org.junit.Assert.assertTrue;
14+import static org.junit.Assert.fail;
15+ 
16+/**
17+ * 连接池初始化与配置相关测试:
18+ * 驱动加载、配置项、初始连接数、错误配置容错
19+ */
20+public class DbcpDataSourceTest extends BaseDbTest {
21+ 
22+ @Test
23+ public void testPoolConfigLoaded() {
24+ Properties props = TestConfig.load();
25+ assertEquals(props.getProperty("url"), dataSource.getUrl());
26+ assertEquals(props.getProperty("username"), dataSource.getUsername());
27+ assertTrue("password 应从配置文件映射到连接池",
28+ props.getProperty("password").equals(dataSource.getPassword()));
29+ assertEquals(props.getProperty("driverClassName"), dataSource.getDriverClassName());
30+ assertEquals(Integer.parseInt(props.getProperty("initialSize")), dataSource.getInitialSize());
31+ assertEquals(Integer.parseInt(props.getProperty("maxTotal")), dataSource.getMaxTotal());
32+ assertEquals(Integer.parseInt(props.getProperty("maxIdle")), dataSource.getMaxIdle());
33+ assertEquals(Integer.parseInt(props.getProperty("minIdle")), dataSource.getMinIdle());
34+ assertEquals(Long.parseLong(props.getProperty("maxWaitMillis")), dataSource.getMaxWaitMillis());
35+ assertEquals(props.getProperty("validationQuery"), dataSource.getValidationQuery());
36+ assertEquals(Boolean.parseBoolean(props.getProperty("testOnBorrow")), dataSource.getTestOnBorrow());
37+ assertEquals(Boolean.parseBoolean(props.getProperty("testWhileIdle")), dataSource.getTestWhileIdle());
38+ assertEquals(Long.parseLong(props.getProperty("timeBetweenEvictionRunsMillis")),
39+ dataSource.getTimeBetweenEvictionRunsMillis());
40+ assertEquals(Long.parseLong(props.getProperty("minEvictableIdleTimeMillis")),
41+ dataSource.getMinEvictableIdleTimeMillis());
42+ assertEquals(Boolean.parseBoolean(props.getProperty("removeAbandonedOnBorrow")),
43+ dataSource.getRemoveAbandonedOnBorrow());
44+ assertEquals(Integer.parseInt(props.getProperty("removeAbandonedTimeout")),
45+ dataSource.getRemoveAbandonedTimeout());
46+ }
47+ 
48+ @Test
49+ public void testDriverExplicitlyLoadable() throws Exception {
50+ // DBCP2 基于 JDBC4 SPI 自动加载驱动,显式 Class.forName 也应可用
51+ Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
52+ assertNotNull(clazz);
53+ }
54+ 
55+ @Test
56+ public void testPoolStartedWithInitialSize() throws Exception {
57+ // 预检已触发建池:initialSize=2,连接全部归还后 idle >= 2
58+ assertEquals(0, dataSource.getNumActive());
59+ assertTrue("idle 应不少于 initialSize,实际: " + dataSource.getNumIdle(),
60+ dataSource.getNumIdle() >= dataSource.getInitialSize());
61+ }
62+ 
63+ @Test
64+ public void testWrongPasswordFails() {
65+ BasicDataSource bad = new BasicDataSource();
66+ applyUrl(bad);
67+ bad.setPassword("definitely-wrong-password");
68+ bad.setMaxTotal(1);
69+ bad.setMaxWaitMillis(3000);
70+ try (Connection conn = bad.getConnection()) {
71+ fail("密码错误时应抛出 SQLException,却成功获取连接: " + conn);
72+ } catch (SQLException e) {
73+ // 预期:认证失败
74+ } finally {
75+ try {
76+ bad.close();
77+ } catch (SQLException ignored) {
78+ }
79+ }
80+ }
81+ 
82+ @Test
83+ public void testWrongPortFails() {
84+ BasicDataSource bad = new BasicDataSource();
85+ bad.setDriverClassName("com.mysql.cj.jdbc.Driver");
86+ // 故意用未监听的端口(原端口 + 1)
87+ String url = TestConfig.load().getProperty("url");
88+ bad.setUrl(url.replaceFirst(":[0-9]+/", ":1/"));
89+ bad.setUsername("mysql_test_db");
90+ bad.setPassword("whatever");
91+ bad.setMaxTotal(1);
92+ bad.setMaxWaitMillis(3000);
93+ try (Connection conn = bad.getConnection()) {
94+ fail("端口不通时应抛出 SQLException,却成功获取连接");
95+ } catch (SQLException e) {
96+ // 预期:连接被拒绝
97+ } finally {
98+ try {
99+ bad.close();
100+ } catch (SQLException ignored) {
101+ }
102+ }
103+ }
104+}
@@ -0,0 +1,243 @@
1+package org.opengauss.test;
2+ 
3+import org.junit.Test;
4+ 
5+import java.math.BigDecimal;
6+import java.sql.Connection;
7+import java.sql.PreparedStatement;
8+import java.sql.ResultSet;
9+import java.sql.SQLException;
10+import java.sql.Statement;
11+ 
12+import static org.junit.Assert.assertEquals;
13+import static org.junit.Assert.assertFalse;
14+import static org.junit.Assert.assertNull;
15+import static org.junit.Assert.assertTrue;
16+ 
17+/**
18+ * Standard SQL category coverage through DBCP and the Dolphin MySQL protocol.
19+ *
20+ * Each test owns a uniquely named table. The DCL test also creates a temporary
21+ * role, verifies the privilege transition, and removes the role in finally so
22+ * repeated mvn test runs do not leave database objects behind.
23+ */
24+public class SqlCategoryTest extends BaseDbTest {
25+ 
26+ /**
27+ * Executes CREATE, ALTER, TRUNCATE, and DROP through one pooled connection.
28+ * JDBC update counts and the inserted row prove that each schema transition
29+ * completed before the next statement; finally provides retry-safe cleanup.
30+ */
31+ @Test
32+ public void testDdlCreateAlterTruncateDrop() throws Exception {
33+ String table = tableName("ddl");
34+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
35+ assertEquals(0, st.executeUpdate("CREATE TABLE " + table
36+ + " (id INT PRIMARY KEY, name VARCHAR(64) NOT NULL)"));
37+ assertEquals(0, st.executeUpdate("ALTER TABLE " + table
38+ + " ADD COLUMN amount DECIMAL(10,2)"));
39+ assertEquals(1, st.executeUpdate("INSERT INTO " + table
40+ + " (id, name, amount) VALUES (1, 'ddl-row', 10.50)"));
41+ assertEquals(0, st.executeUpdate("TRUNCATE TABLE " + table));
42+ assertEquals(0, st.executeUpdate("DROP TABLE " + table));
43+ } finally {
44+ dropTableQuietly(table);
45+ }
46+ }
47+ 
48+ /**
49+ * Verifies INSERT, UPDATE, and DELETE by checking affected-row counts, then
50+ * reads the final state through a separate pooled connection so success is
51+ * not inferred only from the driver's return value.
52+ */
53+ @Test
54+ public void testDmlInsertUpdateDelete() throws Exception {
55+ String table = tableName("dml");
56+ try {
57+ createTable(table);
58+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
59+ assertEquals(2, st.executeUpdate("INSERT INTO " + table
60+ + " (id, name, amount) VALUES (1, 'one', 1.00), (2, 'two', 2.00)"));
61+ assertEquals(1, st.executeUpdate("UPDATE " + table
62+ + " SET amount = 20.00, name = 'two-updated' WHERE id = 2"));
63+ assertEquals(1, st.executeUpdate("DELETE FROM " + table + " WHERE id = 1"));
64+ }
65+ assertEquals(1, countRows(table));
66+ assertEquals("two-updated", findName(table, 2));
67+ } finally {
68+ dropTableQuietly(table);
69+ }
70+ }
71+ 
72+ /**
73+ * Covers parameterized filtering, ORDER BY, and COUNT aggregation. The test
74+ * asserts result order, cardinality, and end-of-result-set behavior to catch
75+ * both missing and unexpected rows.
76+ */
77+ @Test
78+ public void testDqlSelectFilteringOrderingAndAggregate() throws Exception {
79+ String table = tableName("dql");
80+ try {
81+ createTable(table);
82+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
83+ st.executeUpdate("INSERT INTO " + table
84+ + " (id, name, amount) VALUES (1, 'alpha', 3.00), (2, 'beta', 8.00), (3, 'beta-2', 5.00)");
85+ }
86+ try (Connection conn = dataSource.getConnection();
87+ PreparedStatement ps = conn.prepareStatement("SELECT name FROM " + table
88+ + " WHERE amount >= ? ORDER BY amount DESC");
89+ ResultSet rs = setAndQuery(ps, 5.00)) {
90+ assertTrue(rs.next());
91+ assertEquals("beta", rs.getString(1));
92+ assertTrue(rs.next());
93+ assertEquals("beta-2", rs.getString(1));
94+ assertFalse(rs.next());
95+ }
96+ assertEquals(3, countRows(table));
97+ } finally {
98+ dropTableQuietly(table);
99+ }
100+ }
101+ 
102+ /**
103+ * Creates an isolated login role, grants SELECT, checks the effective table
104+ * privilege, revokes it, and checks again. The generated password and unique
105+ * role name prevent credentials or shared database state from entering tests.
106+ */
107+ @Test
108+ public void testDclGrantAndRevoke() throws Exception {
109+ String table = tableName("dcl");
110+ String role = "dbcp_sql_role_" + Long.toUnsignedString(System.nanoTime(), 36);
111+ String rolePassword = "Dbcp!Sql9" + Long.toString(System.nanoTime(), 36);
112+ try {
113+ createTable(table);
114+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
115+ // openGauss requires a password when a role is created through Dolphin.
116+ // Generate a test-only password at runtime; the role is dropped in finally below.
117+ st.executeUpdate("CREATE ROLE " + role + " LOGIN PASSWORD '" + rolePassword + "'");
118+ st.executeUpdate("GRANT SELECT ON TABLE " + table + " TO " + role);
119+ assertTrue(hasTablePrivilege(conn, role, table, "SELECT"));
120+ st.executeUpdate("REVOKE SELECT ON TABLE " + table + " FROM " + role);
121+ assertFalse(hasTablePrivilege(conn, role, table, "SELECT"));
122+ }
123+ } finally {
124+ // Drop the owned table first so a failure between GRANT and REVOKE
125+ // cannot leave dependencies that prevent the temporary role cleanup.
126+ dropTableQuietly(table);
127+ dropRoleQuietly(role);
128+ }
129+ }
130+ 
131+ /**
132+ * Executes explicit START TRANSACTION/COMMIT and START TRANSACTION/ROLLBACK.
133+ * Follow-up reads use new pooled connections, proving committed data is
134+ * visible and rolled-back data is absent outside the original transaction.
135+ */
136+ @Test
137+ public void testTclCommitAndRollback() throws Exception {
138+ String table = tableName("tcl");
139+ try {
140+ createTable(table);
141+ String committed = "committed";
142+ try (Connection conn = dataSource.getConnection(); Statement tx = conn.createStatement()) {
143+ tx.execute("START TRANSACTION");
144+ try (PreparedStatement ps = conn.prepareStatement("INSERT INTO " + table
145+ + " (id, name, amount) VALUES (?, ?, ?)")) {
146+ ps.setInt(1, 1);
147+ ps.setString(2, committed);
148+ ps.setBigDecimal(3, new BigDecimal("1.00"));
149+ assertEquals(1, ps.executeUpdate());
150+ }
151+ tx.execute("COMMIT");
152+ }
153+ assertEquals(1, countRows(table));
154+ 
155+ try (Connection conn = dataSource.getConnection(); Statement tx = conn.createStatement()) {
156+ tx.execute("START TRANSACTION");
157+ try (PreparedStatement ps = conn.prepareStatement("INSERT INTO " + table
158+ + " (id, name, amount) VALUES (?, ?, ?)")) {
159+ ps.setInt(1, 2);
160+ ps.setString(2, "rolled-back");
161+ ps.setBigDecimal(3, new BigDecimal("2.00"));
162+ assertEquals(1, ps.executeUpdate());
163+ }
164+ tx.execute("ROLLBACK");
165+ }
166+ assertEquals(1, countRows(table));
167+ assertEquals("committed", findName(table, 1));
168+ assertNull(findName(table, 2));
169+ } finally {
170+ dropTableQuietly(table);
171+ }
172+ }
173+ 
174+ private String tableName(String category) {
175+ return "dbcp_sql_" + category + "_" + Long.toUnsignedString(System.nanoTime(), 36);
176+ }
177+ 
178+ private void createTable(String table) throws SQLException {
179+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
180+ st.executeUpdate("CREATE TABLE " + table
181+ + " (id INT PRIMARY KEY, name VARCHAR(64) NOT NULL, amount DECIMAL(10,2))");
182+ }
183+ }
184+ 
185+ private int countRows(String table) throws SQLException {
186+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement();
187+ ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM " + table)) {
188+ assertTrue(rs.next());
189+ return rs.getInt(1);
190+ }
191+ }
192+ 
193+ private String findName(String table, int id) throws SQLException {
194+ try (Connection conn = dataSource.getConnection();
195+ PreparedStatement ps = conn.prepareStatement("SELECT name FROM " + table + " WHERE id = ?")) {
196+ ps.setInt(1, id);
197+ try (ResultSet rs = ps.executeQuery()) {
198+ return rs.next() ? rs.getString(1) : null;
199+ }
200+ }
201+ }
202+ 
203+ private ResultSet setAndQuery(PreparedStatement ps, double amount) throws SQLException {
204+ ps.setDouble(1, amount);
205+ return ps.executeQuery();
206+ }
207+ 
208+ private boolean hasTablePrivilege(Connection conn, String role, String table, String privilege)
209+ throws SQLException {
210+ try (PreparedStatement ps = conn.prepareStatement(
211+ "SELECT has_table_privilege(?, ?, ?)")) {
212+ ps.setString(1, role);
213+ ps.setString(2, table);
214+ ps.setString(3, privilege);
215+ try (ResultSet rs = ps.executeQuery()) {
216+ assertTrue(rs.next());
217+ return rs.getBoolean(1);
218+ }
219+ }
220+ }
221+ 
222+ private void dropRoleQuietly(String role) {
223+ if (role == null) {
224+ return;
225+ }
226+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
227+ st.executeUpdate("DROP ROLE " + role);
228+ } catch (SQLException ignored) {
229+ // Cleanup is best effort; preserve the original test failure.
230+ }
231+ }
232+ 
233+ private void dropTableQuietly(String table) {
234+ if (table == null) {
235+ return;
236+ }
237+ try (Connection conn = dataSource.getConnection(); Statement st = conn.createStatement()) {
238+ st.executeUpdate("DROP TABLE IF EXISTS " + table);
239+ } catch (SQLException ignored) {
240+ // Cleanup is best effort; preserve the original test failure.
241+ }
242+ }
243+}