Skip to content

TAP-12846: support SQL query timeout - #954

Closed
jarad0628 wants to merge 12 commits into
developfrom
TAP-12846-query-timeout
Closed

jarad0628 wants to merge 12 commits into
developfrom
TAP-12846-query-timeout

Conversation

@jarad0628

Copy link
Copy Markdown
Collaborator

Summary

  • Add optional queryTimeoutSeconds to executeQuery commands.
  • Apply JDBC Statement.setQueryTimeout() when the value is positive; absent or non-positive values preserve existing behavior.
  • Keep the existing SqlExecutor method compatible with implementations that only implement the original signature.

Validation

  • git diff --check passed.

@jarad0628

Copy link
Copy Markdown
Collaborator Author

AI PR Review — ❌ 不可通行

  • PR: #954
  • Head: a3b7fea87e939879688586e8e08103a1cab90e7a
  • Reviewers: codexcli, reasonix

问题清单

高

  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1131 新的 4 参 execute 分派绕过了 MysqlConnector 的 3 参覆写,MySQL 命令查询丢失流式读取与时间列处理

中

  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:16 新增 default 方法静默丢弃 queryTimeoutSeconds

低

  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1154 新增的 SQL 超时能力无任何测试覆盖
  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1128 batchSize 的 (int) 裸强转在 Long 下会抛 ClassCastException(既有代码,紧邻本次改动)

最终总结

[2m ▎ thinking�[0m
I'll investigate the actual PR checkout to verify the reviewers' findings before arbitrating. Let me start by understanding the change scope.
· 26448 tok · in 25905 (256 cached / 25649 new) · out 543 (323 reasoning) · ¥0.0267
-> bash {"command": "cd /tmp/agentic-ui-pr-954-13706276211275287832/checkout && git log --oneline -5 2>/dev/null; echo "---HEAD...
-> glob {"pattern": "/sql-core/src/main/java/io/tapdata/common/{CommonDbConnector,SqlExecutor,SqlExecuteCommandFunction}.java"...
-> glob {"pattern": "
/MysqlConnector.java"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
Bash sandbox is unavailable on this host; I'll verify using read-only tools. Let me read the changed files.
· 26901 tok · in 26709 (26368 cached / 341 new) · out 192 (20 reasoning) · ¥0.0013
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
�[2m ▎ thinking�[0m
Now let me read CommonDbConnector.java around the changed method and MysqlConnector.
· 28656 tok · in 28403 (26880 cached / 1523 new) · out 253 (23 reasoning) · ¥0.0026
-> grep {"pattern": "execute|queryTimeout|fetchSize|SqlExecutor", "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/...
-> grep {"pattern": "execute|queryTimeout|fetchSize|SqlExecutor|class MysqlConnector", "path": "/tmp/agentic-ui-pr-954-137062762...
�[2m ▎ thinking�[0m
Let me read the full implementations of both execute methods.
· 32202 tok · in 31939 (28544 cached / 3395 new) · out 263 (32 reasoning) · ¥0.0045
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors/mysql-connector/src/main/java/io/tapdata/conne...
�[2m ▎ thinking�[0m
Both P1 and P2 appear real so far. Let me dig into the implications and check for additional issues — implementers of SqlExecutor, the MySQL result-filter difference, and any added tests.
· 36903 tok · in 34742 (32128 cached / 2614 new) · out 2161 (1822 reasoning) · ¥0.0076
-> grep {"pattern": "implements SqlExecutor|extends DefaultSqlExecutor|new DefaultSqlExecutor|SqlExecuteCommandFunction", "path"...
-> grep {"pattern": "filterData|filterTimeForMysql", "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-co...
-> grep {"pattern": "filterTimeForMysql|filterData", "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors/my...
�[2m ▎ thinking�[0m
Confirmed — MysqlConnector.execute uses filterTimeForMysql while the base uses filterData. Now let me read DefaultSqlExecutor and the base filterData to assess the full impact.
· 39150 tok · in 38741 (36864 cached / 1877 new) · out 409 (181 reasoning) · ¥0.0034
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
-> read_file {"limit": 30, "offset": 1420, "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/s...
�[2m ▎ thinking�[0m
DefaultSqlExecutor does override the 6-arg method and applies the timeout. Let me find all SqlExecutor implementers and locate any added tests and the diff.
· 43742 tok · in 43189 (39040 cached / 4149 new) · out 553 (396 reasoning) · ¥0.0060
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "SqlExecutor", "timeout_seconds": 120}
-> ls {"path": "/tmp"}
�[2m ▎ thinking�[0m
There's a patch file. Let me read the PR diff.
· 51197 tok · in 50818 (43648 cached / 7170 new) · out 379 (318 reasoning) · ¥0.0088
-> read_file {"path": "/tmp/tap12846-query-timeout.patch"}
�[2m ▎ thinking�[0m
Now let me find all connectors that override the 3-arg execute (which the new 4-arg call would bypass) and confirm the full changed-file set.
· 53327 tok · in 52766 (51072 cached / 1694 new) · out 561 (341 reasoning) · ¥0.0038
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "void execute\(String sql, Consumer", "time...
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "queryTimeoutSeconds|queryTimeout", "timeout...
�[2m ▎ thinking�[0m
Let me examine filterTimeForMysql to characterize the second-order impact, and confirm no tests were added.
· 57365 tok · in 54574 (53248 cached / 1326 new) · out 2791 (2562 reasoning) · ¥0.0080
-> read_file {"limit": 70, "offset": 638, "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors/mysql-connector/sr...
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "queryTimeoutSeconds|setQueryTimeout|execute...
�[2m ▎ thinking�[0m
Let me confirm the full set of SqlExecutor-shaped implementers (including anonymous/other-interface ones) to scope P2 accurately.
· 65837 tok · in 62502 (57344 cached / 5158 new) · out 3335 (3102 reasoning) · ¥0.0130
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "TapSupplier connectionSupplier,...
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "new SqlExecutor|SqlExecutor\(\)|implement...
�[2m ▎ thinking�[0m
I have verified both reviewer findings against the head source (read-only; bash sandbox is unavailable on this host, so I could not execute tests). Here is the reconciliation.


PR #954 最终裁定报告(head a3b7fea87e939879688586e8e08103a1cab90e7a,TAP-12846: support SQL query timeout)

本轮只有 codexcli 产出发现(2 条,VERDICT: BLOCK);claudecli 因推理网关 503(no available accounts)未产出任何发现。我逐行对照 head 源码核实了 codexcli 的两条发现——均属实且可达,另外补充 2 条低优问题。核心结论:新的超时能力在 CommonDbConnector.executeCommand 路径上回归破坏了 MySQL 的命令查询执行,故维持 BLOCK。

我已核实的变更范围

变更仅 4 个文件,全部在 connectors-common/sql-core:

  • CommonDbConnector.java:executeCommand 改为分派 4 参 execute;新增 4 参 execute(sql, consumer, batchSize, queryTimeoutSeconds) 并在其中 setQueryTimeout。
  • DefaultSqlExecutor.java:3 参 execute 委托到新的 6 参 execute,后者 setQueryTimeout。
  • SqlExecuteCommandFunction.java:解析 queryTimeoutSeconds 并传入新的 6 参 execute。
  • SqlExecutor.java:新增带 queryTimeoutSeconds 的 default 方法。

已接受的发现

级别 位置 问题
高 CommonDbConnector.java:1131 executeCommand 现在调用 4 参 execute。MysqlConnector 只覆写了 3 参 execute(MysqlConnector.java:1220,其中 setFetchSize(Integer.MIN_VALUE) 实现 MySQL 流式读取,并用 filterTimeForMysql 处理时间列)。4 参方法 MysqlConnector 未覆写,因而落到基类实现(CommonDbConnector.java:1151),导致:(1) setFetchSize(Integer.MIN_VALUE) 丢失,改为 setFetchSize(batchSize>0?batchSize:1000)(:1157)——MySQL Connector/J 下大结果集将整体在客户端缓冲,大结果 OOM(原注释即 //protected from OM);(2) MySQL 专用的 filterTimeForMysql 被基类 filterData(:1168,对基类为 no-op)替代,TIME/DATE/DATETIME/TIMESTAMP 列的取值格式/时区行为改变。这是本 PR 引入的回归,且走 supportExecuteCommandFunction(this::executeCommand)(MysqlConnector.java:322)完全可达。
中 SqlExecutor.java:16 新增的 default execute(..., queryTimeoutSeconds) 直接委托回旧签名,静默丢弃 queryTimeoutSeconds。任何未覆写新方法的 SqlExecutor 实现,其超时设置会被无声忽略(对共享库的外部/企业端实现者是行为陷阱)。注:本仓库内唯一实现 DefaultSqlExecutor 已正确覆写 6 参方法,故仓库内实际影响为 0;风险落在仓外实现者。
低 CommonDbConnector.java:1154 新增的超时能力无任何测试:全仓无 queryTimeoutSeconds/setQueryTimeout 相关测试,变更文件也不含测试。executeCommand 传参→setQueryTimeout 应用→超时抛 SQLTimeoutException 被包成 error 结果这条链路无回归保护。
低 CommonDbConnector.java:1128 (int) params.get("batchSize") 的裸强转:若 batchSize 从 JSON 反序列化为 Long 会抛 ClassCastException。(既有代码,本次未改动,紧邻新增的 instanceof Number 更稳健写法,建议一并加固:params.get("batchSize") instanceof Number ? ((Number)...).intValue() : 1000。)

被驳回 / 澄清

  • 无被驳回的实质发现:codexcli 的两条我均已在 head 源码核实为真,未发现可豁免项,也无可用于降级的维护者风险接受评论。
  • claudecli 本轮未产出 finding(服务端 503),不构成第二个独立确认,但 codexcli 的 P1 已由我独立核实。
  • 未发现另外的连接器也覆写了 3 参 execute:全仓仅 MysqlConnector 一处覆写(CommonDbConnector 自身除外),故 P1 的破坏面精确落在 MySQL(及其子类语义)上。
  • 单元层面:setQueryTimeout 单位(秒)与 queryTimeoutSeconds 一致、only if > 0 语义正确(0/负数视为无超时)、instanceof Number 解析稳健——这些非缺陷。

需要做什么

  1. (高,阻塞) 让超时分派不破坏子类覆写:在 MysqlConnector 中覆写新的 4 参 execute(sql, consumer, batchSize, queryTimeoutSeconds)(把 MySQL 的 Integer.MIN_VALUE 流式 + filterTimeForMysql 逻辑迁到 4 参方法,3 参委托到 4 参),或让 executeCommand 仍走 3 参路径由子类自行委托。必须保留 MySQL 的流式读取与时间列处理。
  2. (中) 消除 default 方法的静默丢弃:要么让 default 方法在 queryTimeoutSeconds > 0 且实现未覆写时显式抛出不支持异常/日志告警,要么让 SqlExecutor 的 3 参也带超时语义,避免共享库外部实现者无声失效。
  3. (低) 补充超时链路的测试:executeCommand 传入 queryTimeoutSeconds 后 setQueryTimeout 被调用、超时时返回 error 结果;并覆盖 MySQL 路径仍保持流式与时间列处理。
  4. (低) 加固 batchSize 的解析(instanceof Number),与新增的 queryTimeoutSeconds 解析保持一致。

VERDICT: BLOCK —— 第 1 条是具体的正确性/数据风险回归(MySQL 命令查询在本次改动后走基类实现),未修复前不应合并。


以下是可机读的发现块(供逐条贴到对应改动行):

FINAL_VERDICT: BLOCK
· 71340 tok · in 66313 (65792 cached / 521 new) · out 5027 (2485 reasoning) · ¥0.0119

codexcli review
  • P1 — MySQL command queries lose streaming and can exhaust memory. CommonDbConnector.java:1131 now dispatches to the new four-argument overload. MysqlConnector overrides only the old three-argument method, which sets fetchSize(Integer.MIN_VALUE) to stream MySQL results; the new overload bypasses it and sets a normal fetch size instead. Large execute/executeQuery results may therefore be buffered in memory, including when no timeout is supplied.

  • P2 — Existing custom SqlExecutor implementations silently ignore requested timeouts. SqlExecuteCommandFunction.java:77 passes the timeout, but the new default method in SqlExecutor.java:16 discards it by delegating to the old signature. This preserves source compatibility while making a positive timeout ineffective for implementations that have not overridden the new method.

VERDICT: BLOCK

reasonix review

[2m ▎ thinking�[0m
I'll investigate the actual PR checkout to verify the reviewers' findings before arbitrating. Let me start by understanding the change scope.
· 26448 tok · in 25905 (256 cached / 25649 new) · out 543 (323 reasoning) · ¥0.0267
-> bash {"command": "cd /tmp/agentic-ui-pr-954-13706276211275287832/checkout && git log --oneline -5 2>/dev/null; echo "---HEAD...
-> glob {"pattern": "/sql-core/src/main/java/io/tapdata/common/{CommonDbConnector,SqlExecutor,SqlExecuteCommandFunction}.java"...
-> glob {"pattern": "
/MysqlConnector.java"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
Bash sandbox is unavailable on this host; I'll verify using read-only tools. Let me read the changed files.
· 26901 tok · in 26709 (26368 cached / 341 new) · out 192 (20 reasoning) · ¥0.0013
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
�[2m ▎ thinking�[0m
Now let me read CommonDbConnector.java around the changed method and MysqlConnector.
· 28656 tok · in 28403 (26880 cached / 1523 new) · out 253 (23 reasoning) · ¥0.0026
-> grep {"pattern": "execute|queryTimeout|fetchSize|SqlExecutor", "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/...
-> grep {"pattern": "execute|queryTimeout|fetchSize|SqlExecutor|class MysqlConnector", "path": "/tmp/agentic-ui-pr-954-137062762...
�[2m ▎ thinking�[0m
Let me read the full implementations of both execute methods.
· 32202 tok · in 31939 (28544 cached / 3395 new) · out 263 (32 reasoning) · ¥0.0045
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors/mysql-connector/src/main/java/io/tapdata/conne...
�[2m ▎ thinking�[0m
Both P1 and P2 appear real so far. Let me dig into the implications and check for additional issues — implementers of SqlExecutor, the MySQL result-filter difference, and any added tests.
· 36903 tok · in 34742 (32128 cached / 2614 new) · out 2161 (1822 reasoning) · ¥0.0076
-> grep {"pattern": "implements SqlExecutor|extends DefaultSqlExecutor|new DefaultSqlExecutor|SqlExecuteCommandFunction", "path"...
-> grep {"pattern": "filterData|filterTimeForMysql", "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-co...
-> grep {"pattern": "filterTimeForMysql|filterData", "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors/my...
�[2m ▎ thinking�[0m
Confirmed — MysqlConnector.execute uses filterTimeForMysql while the base uses filterData. Now let me read DefaultSqlExecutor and the base filterData to assess the full impact.
· 39150 tok · in 38741 (36864 cached / 1877 new) · out 409 (181 reasoning) · ¥0.0034
-> read_file {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/src/main/java/io/tapdata/commo...
-> read_file {"limit": 30, "offset": 1420, "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors-common/sql-core/s...
�[2m ▎ thinking�[0m
DefaultSqlExecutor does override the 6-arg method and applies the timeout. Let me find all SqlExecutor implementers and locate any added tests and the diff.
· 43742 tok · in 43189 (39040 cached / 4149 new) · out 553 (396 reasoning) · ¥0.0060
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "SqlExecutor", "timeout_seconds": 120}
-> ls {"path": "/tmp"}
�[2m ▎ thinking�[0m
There's a patch file. Let me read the PR diff.
· 51197 tok · in 50818 (43648 cached / 7170 new) · out 379 (318 reasoning) · ¥0.0088
-> read_file {"path": "/tmp/tap12846-query-timeout.patch"}
�[2m ▎ thinking�[0m
Now let me find all connectors that override the 3-arg execute (which the new 4-arg call would bypass) and confirm the full changed-file set.
· 53327 tok · in 52766 (51072 cached / 1694 new) · out 561 (341 reasoning) · ¥0.0038
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "void execute\(String sql, Consumer", "time...
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "queryTimeoutSeconds|queryTimeout", "timeout...
�[2m ▎ thinking�[0m
Let me examine filterTimeForMysql to characterize the second-order impact, and confirm no tests were added.
· 57365 tok · in 54574 (53248 cached / 1326 new) · out 2791 (2562 reasoning) · ¥0.0080
-> read_file {"limit": 70, "offset": 638, "path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout/connectors/mysql-connector/sr...
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "queryTimeoutSeconds|setQueryTimeout|execute...
�[2m ▎ thinking�[0m
Let me confirm the full set of SqlExecutor-shaped implementers (including anonymous/other-interface ones) to scope P2 accurately.
· 65837 tok · in 62502 (57344 cached / 5158 new) · out 3335 (3102 reasoning) · ¥0.0130
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "TapSupplier connectionSupplier,...
-> grep {"path": "/tmp/agentic-ui-pr-954-13706276211275287832/checkout", "pattern": "new SqlExecutor|SqlExecutor\(\)|implement...
�[2m ▎ thinking�[0m
I have verified both reviewer findings against the head source (read-only; bash sandbox is unavailable on this host, so I could not execute tests). Here is the reconciliation.


PR #954 最终裁定报告(head a3b7fea87e939879688586e8e08103a1cab90e7a,TAP-12846: support SQL query timeout)

本轮只有 codexcli 产出发现(2 条,VERDICT: BLOCK);claudecli 因推理网关 503(no available accounts)未产出任何发现。我逐行对照 head 源码核实了 codexcli 的两条发现——均属实且可达,另外补充 2 条低优问题。核心结论:新的超时能力在 CommonDbConnector.executeCommand 路径上回归破坏了 MySQL 的命令查询执行,故维持 BLOCK。

我已核实的变更范围

变更仅 4 个文件,全部在 connectors-common/sql-core:

  • CommonDbConnector.java:executeCommand 改为分派 4 参 execute;新增 4 参 execute(sql, consumer, batchSize, queryTimeoutSeconds) 并在其中 setQueryTimeout。
  • DefaultSqlExecutor.java:3 参 execute 委托到新的 6 参 execute,后者 setQueryTimeout。
  • SqlExecuteCommandFunction.java:解析 queryTimeoutSeconds 并传入新的 6 参 execute。
  • SqlExecutor.java:新增带 queryTimeoutSeconds 的 default 方法。

已接受的发现

级别 位置 问题
高 CommonDbConnector.java:1131 executeCommand 现在调用 4 参 execute。MysqlConnector 只覆写了 3 参 execute(MysqlConnector.java:1220,其中 setFetchSize(Integer.MIN_VALUE) 实现 MySQL 流式读取,并用 filterTimeForMysql 处理时间列)。4 参方法 MysqlConnector 未覆写,因而落到基类实现(CommonDbConnector.java:1151),导致:(1) setFetchSize(Integer.MIN_VALUE) 丢失,改为 setFetchSize(batchSize>0?batchSize:1000)(:1157)——MySQL Connector/J 下大结果集将整体在客户端缓冲,大结果 OOM(原注释即 //protected from OM);(2) MySQL 专用的 filterTimeForMysql 被基类 filterData(:1168,对基类为 no-op)替代,TIME/DATE/DATETIME/TIMESTAMP 列的取值格式/时区行为改变。这是本 PR 引入的回归,且走 supportExecuteCommandFunction(this::executeCommand)(MysqlConnector.java:322)完全可达。
中 SqlExecutor.java:16 新增的 default execute(..., queryTimeoutSeconds) 直接委托回旧签名,静默丢弃 queryTimeoutSeconds。任何未覆写新方法的 SqlExecutor 实现,其超时设置会被无声忽略(对共享库的外部/企业端实现者是行为陷阱)。注:本仓库内唯一实现 DefaultSqlExecutor 已正确覆写 6 参方法,故仓库内实际影响为 0;风险落在仓外实现者。
低 CommonDbConnector.java:1154 新增的超时能力无任何测试:全仓无 queryTimeoutSeconds/setQueryTimeout 相关测试,变更文件也不含测试。executeCommand 传参→setQueryTimeout 应用→超时抛 SQLTimeoutException 被包成 error 结果这条链路无回归保护。
低 CommonDbConnector.java:1128 (int) params.get("batchSize") 的裸强转:若 batchSize 从 JSON 反序列化为 Long 会抛 ClassCastException。(既有代码,本次未改动,紧邻新增的 instanceof Number 更稳健写法,建议一并加固:params.get("batchSize") instanceof Number ? ((Number)...).intValue() : 1000。)

被驳回 / 澄清

  • 无被驳回的实质发现:codexcli 的两条我均已在 head 源码核实为真,未发现可豁免项,也无可用于降级的维护者风险接受评论。
  • claudecli 本轮未产出 finding(服务端 503),不构成第二个独立确认,但 codexcli 的 P1 已由我独立核实。
  • 未发现另外的连接器也覆写了 3 参 execute:全仓仅 MysqlConnector 一处覆写(CommonDbConnector 自身除外),故 P1 的破坏面精确落在 MySQL(及其子类语义)上。
  • 单元层面:setQueryTimeout 单位(秒)与 queryTimeoutSeconds 一致、only if > 0 语义正确(0/负数视为无超时)、instanceof Number 解析稳健——这些非缺陷。

需要做什么

  1. (高,阻塞) 让超时分派不破坏子类覆写:在 MysqlConnector 中覆写新的 4 参 execute(sql, consumer, batchSize, queryTimeoutSeconds)(把 MySQL 的 Integer.MIN_VALUE 流式 + filterTimeForMysql 逻辑迁到 4 参方法,3 参委托到 4 参),或让 executeCommand 仍走 3 参路径由子类自行委托。必须保留 MySQL 的流式读取与时间列处理。
  2. (中) 消除 default 方法的静默丢弃:要么让 default 方法在 queryTimeoutSeconds > 0 且实现未覆写时显式抛出不支持异常/日志告警,要么让 SqlExecutor 的 3 参也带超时语义,避免共享库外部实现者无声失效。
  3. (低) 补充超时链路的测试:executeCommand 传入 queryTimeoutSeconds 后 setQueryTimeout 被调用、超时时返回 error 结果;并覆盖 MySQL 路径仍保持流式与时间列处理。
  4. (低) 加固 batchSize 的解析(instanceof Number),与新增的 queryTimeoutSeconds 解析保持一致。

VERDICT: BLOCK —— 第 1 条是具体的正确性/数据风险回归(MySQL 命令查询在本次改动后走基类实现),未修复前不应合并。


以下是可机读的发现块(供逐条贴到对应改动行):

===FINDINGS===
[高] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1131
新的 4 参 execute 分派绕过了 MysqlConnector 的 3 参覆写,MySQL 命令查询丢失流式读取与时间列处理
executeCommand(:1131)现在调用 4 参 execute(sql, …, batchSize, queryTimeoutSeconds)(新方法见 :1151)。但 MysqlConnector 只覆写了 3 参 execute(MysqlConnector.java:1220),其中 setFetchSize(Integer.MIN_VALUE) 是 MySQL Connector/J 的流式读取开关,并用 filterTimeForMysql 处理 TIME/DATE/DATETIME/TIMESTAMP 列。4 参方法未被覆写,动态分派落到基类实现(:1157 用 setFetchSize(batchSize>0?batchSize:1000);:1168 用 no-op 的 filterData)。后果:(1) 大结果集在客户端整体缓冲,存在 OOM 风险(此行原本的注释即 //protected from OM);(2) MySQL 时间列取值格式/时区行为改变。该路径经 supportExecuteCommandFunction(this::executeCommand)(MysqlConnector.java:322)完全可达,属本次改动引入的回归。
建议:在 MysqlConnector 中覆写新的 4 参 execute(保留 Integer.MIN_VALUE 流式与 filterTimeForMysql,让 3 参委托到 4 参),或让 executeCommand 继续走 3 参路径由子类委托;并补「MySQL 命令查询仍流式 + 时间列处理不变」的测试。

[中] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:16
新增 default 方法静默丢弃 queryTimeoutSeconds
default execute(..., queryTimeoutSeconds)(:16)直接委托回旧签名 execute(..., batchSize)(:18),把 queryTimeoutSeconds 静默丢弃。任何未覆写新方法的 SqlExecutor 实现,其请求的超时会被无声忽略(SqlExecuteCommandFunction.java:81 明确传入了该参数)。本仓库内唯一实现 DefaultSqlExecutor(DefaultSqlExecutor.java:50)已覆写 6 参方法并生效,故仓库内无实际破坏;风险位于共享库的外部/企业端实现者。
建议:default 方法在 queryTimeoutSeconds > 0 且实现未覆写时抛出不支持异常或记录告警,避免静默失效;或在接口层即为 3 参方法补超时语义。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1154
新增的 SQL 超时能力无任何测试覆盖
变更仅落在 4 个 main 文件,仓库内无任何针对 queryTimeoutSeconds/setQueryTimeout 的测试。新增的解析(:1129-1130)、传参(:1131-1132)与应用(:1154-1156)这条链路无回归保护,正是发现 1 未被测试暴露的原因。
建议:新增测试覆盖「executeCommand 传入正超时 → setQueryTimeout 被调用」与「超时时返回 error 结果」,并覆盖 MySQL 子类的流式与时间列行为保持。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1128
batchSize 的 (int) 裸强转在 Long 下会抛 ClassCastException(既有代码,紧邻本次改动)
int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;(:1128)。若 params 来自 JSON 反序列化且 batchSize 被解析为 Long,该强制转换会抛 ClassCastException。此行本次未改动,但紧邻新增的 queryTimeoutSeconds 已改用稳健的 instanceof Number 写法,二者不一致。
建议:与 queryTimeoutSeconds 解析保持一致,改为 params.get("batchSize") instanceof Number ? ((Number) params.get("batchSize")).intValue() : 1000。
===END===

FINAL_VERDICT: BLOCK
· 71340 tok · in 66313 (65792 cached / 521 new) · out 5027 (2485 reasoning) · ¥0.0119

标记:❌ 不可通行

execute(sql, list -> consumer.accept(new ExecuteResult().result(list)), batchSize);
int queryTimeoutSeconds = params.get("queryTimeoutSeconds") instanceof Number
? ((Number) params.get("queryTimeoutSeconds")).intValue() : 0;
execute(sql, list -> consumer.accept(new ExecuteResult().result(list)), batchSize,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[高] 新的 4 参 execute 分派绕过了 MysqlConnector 的 3 参覆写,MySQL 命令查询丢失流式读取与时间列处理

executeCommand(:1131)现在调用 4 参 execute(sql, …, batchSize, queryTimeoutSeconds)(新方法见 :1151)。但 MysqlConnector 只覆写了 3 参 execute(MysqlConnector.java:1220),其中 setFetchSize(Integer.MIN_VALUE) 是 MySQL Connector/J 的流式读取开关,并用 filterTimeForMysql 处理 TIME/DATE/DATETIME/TIMESTAMP 列。4 参方法未被覆写,动态分派落到基类实现(:1157 用 setFetchSize(batchSize>0?batchSize:1000);:1168 用 no-op 的 filterData)。后果:(1) 大结果集在客户端整体缓冲,存在 OOM 风险(此行原本的注释即 //protected from OM);(2) MySQL 时间列取值格式/时区行为改变。该路径经 supportExecuteCommandFunction(this::executeCommand)(MysqlConnector.java:322)完全可达,属本次改动引入的回归。
建议:在 MysqlConnector 中覆写新的 4 参 execute(保留 Integer.MIN_VALUE 流式与 filterTimeForMysql,让 3 参委托到 4 参),或让 executeCommand 继续走 3 参路径由子类委托;并补「MySQL 命令查询仍流式 + 时间列处理不变」的测试。


void execute(String sql, TapSupplier<Connection> connectionSupplier, Consumer<Object> consumer, Supplier<Boolean> aliveSupplier, int batchSize) throws Throwable;

default void execute(String sql, TapSupplier<Connection> connectionSupplier, Consumer<Object> consumer,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 新增 default 方法静默丢弃 queryTimeoutSeconds

default execute(..., queryTimeoutSeconds)(:16)直接委托回旧签名 execute(..., batchSize)(:18),把 queryTimeoutSeconds 静默丢弃。任何未覆写新方法的 SqlExecutor 实现,其请求的超时会被无声忽略(SqlExecuteCommandFunction.java:81 明确传入了该参数)。本仓库内唯一实现 DefaultSqlExecutor(DefaultSqlExecutor.java:50)已覆写 6 参方法并生效,故仓库内无实际破坏;风险位于共享库的外部/企业端实现者。
建议:default 方法在 queryTimeoutSeconds > 0 且实现未覆写时抛出不支持异常或记录告警,避免静默失效;或在接口层即为 3 参方法补超时语义。

protected void execute(String sql, Consumer<Object> consumer, int batchSize, int queryTimeoutSeconds) throws Throwable {
try (Connection connection = jdbcContext.getConnection();
Statement sqlStatement = connection.createStatement()) {
if (queryTimeoutSeconds > 0) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 新增的 SQL 超时能力无任何测试覆盖

变更仅落在 4 个 main 文件,仓库内无任何针对 queryTimeoutSeconds/setQueryTimeout 的测试。新增的解析(:1129-1130)、传参(:1131-1132)与应用(:1154-1156)这条链路无回归保护,正是发现 1 未被测试暴露的原因。
建议:新增测试覆盖「executeCommand 传入正超时 → setQueryTimeout 被调用」与「超时时返回 error 结果」,并覆盖 MySQL 子类的流式与时间列行为保持。

@@ -1126,7 +1126,10 @@
case "executeQuery":
String sql = (String) params.get("sql");
int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] batchSize 的 (int) 裸强转在 Long 下会抛 ClassCastException(既有代码,紧邻本次改动)

int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;(:1128)。若 params 来自 JSON 反序列化且 batchSize 被解析为 Long,该强制转换会抛 ClassCastException。此行本次未改动,但紧邻新增的 queryTimeoutSeconds 已改用稳健的 instanceof Number 写法,二者不一致。
建议:与 queryTimeoutSeconds 解析保持一致,改为 params.get("batchSize") instanceof Number ? ((Number) params.get("batchSize")).intValue() : 1000。

@jarad0628
jarad0628 marked this pull request as draft October 16, 2026 02:12
@jarad0628

Copy link
Copy Markdown
Collaborator Author

AI PR Review — ✅ 可通行

  • PR: #954
  • Head: 3f0cea121865d15d8f92e0a6686b9cd73fa46ddf
  • Reviewers: codexcli, claudecli, reasonix

问题清单

中

  • connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:70 新增测试只覆盖 MySQL 一条路径,共享实现与异常路径零测试

低

  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1129 queryTimeoutSeconds 与 batchSize 的入参校验策略相反,字符串型超时会被静默忽略
  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1130 intValue() 对超 int 的数值回绕成负数,超时被静默关闭
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:77 同一段参数解析逻辑被复制到第二处,修正时容易漏改
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:79 call(存储过程/函数)路径完全没有超时,PR 目标在这条执行路径上不完整
  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1147 三参 execute 退化为纯委派,只覆盖三参版本的外部子类会被静默旁路
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:17 新增的 default 重载会静默丢弃超时,且接口层没有任何契约说明
  • connectors/mysql-connector/src/main/java/io/tapdata/connector/mysql/MysqlConnector.java:1230 MySQL 流式读取下的超时语义未经验证,且超时同样会作用在逐行 fetch 阶段

最终总结

[2m ▎ thinking�[0m
· 27235 tok · in 27040 (256 cached / 26784 new) · out 195 (58 reasoning) · ¥0.0272
-> bash {"command": "cd /tmp/agentic-ui-pr-954-248394612134822629/checkout && pwd && git log --oneline -3 2>&1 | head -5"}
-> bash {"command": "cd /tmp/agentic-ui-pr-954-248394612134822629/checkout && git diff --stat HEAD~1 2>&1 | head -30"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
· 27532 tok · in 27375 (27136 cached / 239 new) · out 157 (19 reasoning) · ¥0.0011
-> grep {"pattern": "queryTimeoutSeconds", "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout"}
-> grep {"pattern": "setQueryTimeout", "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout"}
�[2m ▎ thinking�[0m
· 29114 tok · in 28926 (27520 cached / 1406 new) · out 188 (43 reasoning) · ¥0.0023
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
· 33296 tok · in 33083 (29056 cached / 4027 new) · out 213 · ¥0.0050
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
�[2m ▎ thinking�[0m
· 35695 tok · in 35164 (33280 cached / 1884 new) · out 531 (350 reasoning) · ¥0.0036
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "void execute\(String sql"}
-> read_file {"limit": 70, "offset": 1190, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors/mysql-connector/src...
�[2m ▎ thinking�[0m
· 37979 tok · in 37634 (35584 cached / 2050 new) · out 345 (195 reasoning) · ¥0.0035
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "implements SqlExecutor|new SqlExecutor|SqlExe...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "executeCommand\("}
�[2m ▎ thinking�[0m
· 40517 tok · in 39900 (37888 cached / 2012 new) · out 617 (476 reasoning) · ¥0.0040
-> ls {"path": "/tmp/agentic-ui-pr-954-248394612134822629"}
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors/mysql-connector/src/test/java/io/tapdata/connect...
�[2m ▎ thinking�[0m
· 44476 tok · in 42223 (40448 cached / 1775 new) · out 2253 (2059 reasoning) · ¥0.0071
-> read_file {"limit": 70, "offset": 225, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/...
-> read_file {"limit": 40, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/ta...
�[2m ▎ thinking�[0m
· 45903 tok · in 45563 (44416 cached / 1147 new) · out 340 (190 reasoning) · ¥0.0027
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "get\(\"batchSize\"\)"}
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "queryTimeout|query_timeout|\"timeout\""}
�[2m ▎ thinking�[0m
· 48618 tok · in 48239 (45824 cached / 2415 new) · out 379 (255 reasoning) · ¥0.0041
-> glob {"pattern": "connectors-common/sql-core/src/test/java/io/tapdata/common/*.java"}
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "DefaultSqlExecutor|SqlExecuteCommandFunction"...
�[2m ▎ thinking�[0m
· 50633 tok · in 49964 (48512 cached / 1452 new) · out 669 (496 reasoning) · ¥0.0038
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "executeCommand\(connectorContext|this::execu...
�[2m ▎ thinking�[0m
· 52558 tok · in 52151 (50560 cached / 1591 new) · out 407 (295 reasoning) · ¥0.0034
-> read_file {"limit": 70, "offset": 1430, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src...
�[2m ▎ thinking�[0m
· 54113 tok · in 52892 (52480 cached / 412 new) · out 1221 (1029 reasoning) · ¥0.0039
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors/mysql-connector/src/main/java/io/tapdata/connect...
�[2m ▎ thinking�[0m
· 55120 tok · in 54886 (54016 cached / 870 new) · out 234 (82 reasoning) · ¥0.0024
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common", "pattern": "AtomicBoolean isAlive|isAli...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "boolean isAlive\("}
�[2m ▎ thinking�[0m
· 59728 tok · in 57036 (55040 cached / 1996 new) · out 2692 (2595 reasoning) · ¥0.0085
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/mysql-core/src/main/java/io/tapdata/conne...
· tool output truncated: 5289 of 38057 bytes elided
�[2m ▎ thinking�[0m

核对方式

本机 bash 沙箱不可用(bash sandbox requested but unavailable on this host),无法执行 git diff / mvn test,因此我改为逐文件静态核对 head 工作区,并把范围钉在「含 queryTimeoutSeconds 的 6 个文件」上(与 codexcli 所述"五个实现文件 + 一个测试"一致):SqlExecutor.java(全文 25 行)、DefaultSqlExecutor.java:44-83、CommonDbConnector.java:1120-1184(另核对 executeCommandV2:1438-1448、JdbcContext:235-279)、SqlExecuteCommandFunction.java(全文 95 行)、MysqlConnector.java:1220-1258,1260-1310、MysqlConnectorTest.java:46-74;并用全仓 grep 确认了参数名、调用链与覆盖范围。

仲裁结论

两位 reviewer 的 PASS 结论我接受:改动方向正确(超时只在该语句新建的 Statement 上、> 0 才设置、try-with-resources 保证释放、MySQL 的 setFetchSize(Integer.MIN_VALUE) 流式语义原样保留),并且我额外核实了覆盖面比两位 reviewer 说的更广:CommonDbConnector.executeCommand 经 this::executeCommand 被 clickhouse/doris/dws/greenplum/highgo/opengauss/postgres/starrocks/tidb/vastbase/oceanbase/mysql 等约 20 个连接器注册,SqlExecuteCommandFunction 则覆盖 hive/hive1/hudi/snowflake/databend,因此新参数在这两条主链上都能到达 setQueryTimeout;仓库内只有 DefaultSqlExecutor 实现 SqlExecutor,不存在"默认方法静默吞掉超时"的实际路径;MysqlConnector 是唯一覆盖 execute(...) 的子类且已同步改造,无子类被新增重载旁路;MysqlConfig 未设 enableQueryTimeouts=false,Connector/J 默认超时机制可用。

同时确认两位都只给了 Low,但代码里确实存在若干"静默失效"路径,属于必须写清楚但不必阻塞的问题。

已接受 findings(1 中 + 6 低)

  • 中:测试只覆盖 MysqlConnector 一条(且只断言 setQueryTimeout(8) 被调用);承载几十个连接器的主路径 CommonDbConnector.execute、DefaultSqlExecutor.execute、SqlExecuteCommandFunction 的参数解析零单测,也没有"超时后抛 SQLException / <=0 不设置超时"的断言。
  • 低:queryTimeoutSeconds 用 instanceof Number 静默回落 0,而紧邻的 batchSize 用 (int) 强转——同一份 params 两个字段策略相反,字符串型超时值会被静默忽略(防护特性静默失效)。
  • 低:((Number) v).intValue() 对超 int 的 Long(如 2147483648)回绕成负数,被 > 0 判空,超时同样静默失效。
  • 低:上述解析逻辑在 CommonDbConnector:1129 与 SqlExecuteCommandFunction:77 被复制两份。
  • 低:call(存储过程/函数)命令路径完全没有超时(MysqlConnector.call:1275/1305 与 DefaultSqlExecutor.call:133 的 CallableStatement 均未设),PR 目标在这条执行路径上不完整。
  • 低:CommonDbConnector 的三参 execute 退化为委派后,executeCommand 改调四参版本——只覆盖三参 execute 的子类(改造前唯一的扩展点)会被整体旁路(不只是丢超时)。仓库内已同步,属隐蔽的未来隐患。
  • 低:queryTimeoutSeconds 无任何 javadoc/契约说明(秒、0 与负数=不限、超时抛 SQLException),而它是暴露在 execute command params 里的公共契约。
  • 低:MySQL 流式场景缺集成验证——setQueryTimeout 只对语句生效、Connector/J 依赖独立连接 KILL QUERY,而超时同样作用于逐行 fetch 阶段,长表全扫可能被判超时;单测证明不了这点。

未采纳 / 已澄清项

  • codexcli「无问题」:结论方向我认同(无回归、旧实现仍可经默认重载调用),但它完全没有覆盖"参数解析静默失效"与"测试只打了一条桩"这两块,因此不作为"零发现"采纳。
  • claudecli「Integer overflow 会 silently bypass」:问题成立,但把它归为"用户以为设了上限"的场景不准确——真正可达的输入是 JSON 传入 Long;已按实际可达路径改写并降为低。
  • claudecli「负值按 0 处理」:不采纳为缺陷,setQueryTimeout(0) 即"无限制",与 JDBC 语义一致。
  • batchSize 的 (int) 强转在 SqlExecuteCommandFunction:76 会 ClassCastException:该行不在本次改动内,属既有问题,仅作上下文提示。
  • executeCommandV2(CommonDbConnector:1438)无超时:其函数签名根本没有 params 通道,属既有 API 限制,不计入本 PR 缺陷。

下一步

① 把两处 params 解析抽成一个 helper(兼容 String、clamp 到 Integer.MAX_VALUE、非法值打日志),供 CommonDbConnector 与 SqlExecuteCommandFunction 共用;② 给 queryTimeoutSeconds 补 javadoc(SqlExecutor 接口 + 两处实现),并把三参 execute 标 @Deprecated 说明必须覆盖四参;③ 补单测:CommonDbConnector.execute 与 DefaultSqlExecutor.execute 的"设置/不设置 setQueryTimeout"、"超时抛 SQLException 后 statement 与 connection 均被关闭"、"字符串/超 int 参数"三类;④ 决定 call 路径是否也要支持超时(支持则在 call 分支透传);⑤ 有余力补一条 MySQL IT 验证流式 + 超时确实生效。

FINAL_VERDICT: PASS

· 76251 tok · in 69600 (59648 cached / 9952 new) · out 6651 (2736 reasoning) · ¥0.0244

codexcli review

No concrete issues found in the five reviewable implementation files. The timeout reaches the default SQL executor and both common and MySQL connector execution paths; absent or non-positive values preserve the existing behavior. Legacy SqlExecutor implementations remain callable through the default overload.

VERDICT: PASS

claudecli review

Looking at PR #954 which adds optional SQL query timeout support across the connector framework.

Code Analysis

Architecture & Design

✅ Good backward compatibility: Uses method overloading and interface default methods to avoid breaking existing implementations
✅ Consistent application: Timeout applied at the JDBC Statement level before execution
✅ MySQL-specific behavior preserved: MysqlConnector correctly maintains setFetchSize(Integer.MIN_VALUE) streaming

Findings

[Low] CommonDbConnector.java:1129 & SqlExecuteCommandFunction.java:77 — Silent type handling inconsistency

int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;
int queryTimeoutSeconds = params.get("queryTimeoutSeconds") instanceof Number
        ? ((Number) params.get("queryTimeoutSeconds")).intValue() : 0;

batchSize uses null-check with direct cast (will throw ClassCastException if wrong type), while queryTimeoutSeconds uses instanceof Number (silently defaults to 0 if wrong type). This inconsistency means:

  • Passing {"batchSize": "1000"} → runtime exception
  • Passing {"queryTimeoutSeconds": "30"} → silently ignored, no timeout applied

Impact: String timeout values from external configs fail silently. Given that query timeout is defensive (preventing runaway queries), silent failure means expected protection doesn't activate.

Recommendation: Either make both lenient (instanceof Number for both) or strict (fail fast for both), depending on input validation strategy elsewhere in the codebase.


[Low] CommonDbConnector.java:1130 — Integer overflow wraps to negative, bypassing timeout

int queryTimeoutSeconds = params.get("queryTimeoutSeconds") instanceof Number
        ? ((Number) params.get("queryTimeoutSeconds")).intValue() : 0;
if (queryTimeoutSeconds > 0) {

If params contains a Long like 2147483648L (Integer.MAX_VALUE + 1), intValue() wraps to -2147483648, failing the > 0 check, so no timeout is applied. While edge-case, this could confuse users who pass large values expecting "effectively infinite but still a limit."

Recommendation: Either clamp to Integer.MAX_VALUE or document that values must fit in a 32-bit signed int.


[Low] Missing inline documentation — Timeout behavior not commented

The parameter name queryTimeoutSeconds is self-documenting, but these aren't obvious:

  • What exception is thrown when timeout exceeded (SQLException)
  • That 0 means no limit (JDBC spec)
  • That negative values are treated as 0 (no limit) due to the > 0 guard

Recommendation: Add javadoc or inline comment explaining JDBC timeout semantics, especially since this is a public API parameter.


[Low] MysqlConnectorTest.java:68 — Test doesn't verify timeout exception behavior

Test verifies setQueryTimeout(8) is called and normal execution flow works, but doesn't test:

  • What happens when a query actually times out (should throw SQLException per JDBC spec)
  • Whether cleanup (connection close) still happens after timeout exception

Recommendation: Add a test with a mock that simulates timeout exception to ensure exception handling path is correct.


Positive Observations

✅ Thread-safe: Statement is method-local and try-with-resources scoped
✅ Fail-safe defaults: Missing or invalid timeout values default to 0 (no timeout), preserving existing behavior
✅ Security positive: Query timeout is a defensive feature preventing resource exhaustion
✅ Interface evolution done right: Default method in SqlExecutor.java allows gradual adoption
✅ Test verifies MySQL quirks: Confirms Integer.MIN_VALUE fetchSize and filterTimeForMysql still work

Correctness Check

  • ✅ Timeout only applied when > 0
  • ✅ JDBC setQueryTimeout() takes seconds, matches parameter name
  • ✅ Works with try-with-resources resource management
  • ✅ MysqlConnector's 3-param version correctly delegates to 4-param version
  • ✅ No race conditions (Statement not shared across threads)

The implementation is functionally correct and safe. The findings are polish issues around input validation consistency and documentation.

VERDICT: PASS

reasonix review

[2m ▎ thinking�[0m
· 27235 tok · in 27040 (256 cached / 26784 new) · out 195 (58 reasoning) · ¥0.0272
-> bash {"command": "cd /tmp/agentic-ui-pr-954-248394612134822629/checkout && pwd && git log --oneline -3 2>&1 | head -5"}
-> bash {"command": "cd /tmp/agentic-ui-pr-954-248394612134822629/checkout && git diff --stat HEAD~1 2>&1 | head -30"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
· 27532 tok · in 27375 (27136 cached / 239 new) · out 157 (19 reasoning) · ¥0.0011
-> grep {"pattern": "queryTimeoutSeconds", "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout"}
-> grep {"pattern": "setQueryTimeout", "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout"}
�[2m ▎ thinking�[0m
· 29114 tok · in 28926 (27520 cached / 1406 new) · out 188 (43 reasoning) · ¥0.0023
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
· 33296 tok · in 33083 (29056 cached / 4027 new) · out 213 · ¥0.0050
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
�[2m ▎ thinking�[0m
· 35695 tok · in 35164 (33280 cached / 1884 new) · out 531 (350 reasoning) · ¥0.0036
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "void execute\(String sql"}
-> read_file {"limit": 70, "offset": 1190, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors/mysql-connector/src...
�[2m ▎ thinking�[0m
· 37979 tok · in 37634 (35584 cached / 2050 new) · out 345 (195 reasoning) · ¥0.0035
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "implements SqlExecutor|new SqlExecutor|SqlExe...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "executeCommand\("}
�[2m ▎ thinking�[0m
· 40517 tok · in 39900 (37888 cached / 2012 new) · out 617 (476 reasoning) · ¥0.0040
-> ls {"path": "/tmp/agentic-ui-pr-954-248394612134822629"}
-> read_file {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors/mysql-connector/src/test/java/io/tapdata/connect...
�[2m ▎ thinking�[0m
· 44476 tok · in 42223 (40448 cached / 1775 new) · out 2253 (2059 reasoning) · ¥0.0071
-> read_file {"limit": 70, "offset": 225, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/...
-> read_file {"limit": 40, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/ta...
�[2m ▎ thinking�[0m
· 45903 tok · in 45563 (44416 cached / 1147 new) · out 340 (190 reasoning) · ¥0.0027
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "get\(\"batchSize\"\)"}
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "queryTimeout|query_timeout|\"timeout\""}
�[2m ▎ thinking�[0m
· 48618 tok · in 48239 (45824 cached / 2415 new) · out 379 (255 reasoning) · ¥0.0041
-> glob {"pattern": "connectors-common/sql-core/src/test/java/io/tapdata/common/*.java"}
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "DefaultSqlExecutor|SqlExecuteCommandFunction"...
�[2m ▎ thinking�[0m
· 50633 tok · in 49964 (48512 cached / 1452 new) · out 669 (496 reasoning) · ¥0.0038
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "executeCommand\(connectorContext|this::execu...
�[2m ▎ thinking�[0m
· 52558 tok · in 52151 (50560 cached / 1591 new) · out 407 (295 reasoning) · ¥0.0034
-> read_file {"limit": 70, "offset": 1430, "path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src...
�[2m ▎ thinking�[0m
· 54113 tok · in 52892 (52480 cached / 412 new) · out 1221 (1029 reasoning) · ¥0.0039
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common/...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors/mysql-connector/src/main/java/io/tapdata/connect...
�[2m ▎ thinking�[0m
· 55120 tok · in 54886 (54016 cached / 870 new) · out 234 (82 reasoning) · ¥0.0024
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common", "pattern": "AtomicBoolean isAlive|isAli...
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout", "pattern": "boolean isAlive\("}
�[2m ▎ thinking�[0m
· 59728 tok · in 57036 (55040 cached / 1996 new) · out 2692 (2595 reasoning) · ¥0.0085
-> grep {"path": "/tmp/agentic-ui-pr-954-248394612134822629/checkout/connectors-common/mysql-core/src/main/java/io/tapdata/conne...
· tool output truncated: 5289 of 38057 bytes elided
�[2m ▎ thinking�[0m

核对方式

本机 bash 沙箱不可用(bash sandbox requested but unavailable on this host),无法执行 git diff / mvn test,因此我改为逐文件静态核对 head 工作区,并把范围钉在「含 queryTimeoutSeconds 的 6 个文件」上(与 codexcli 所述"五个实现文件 + 一个测试"一致):SqlExecutor.java(全文 25 行)、DefaultSqlExecutor.java:44-83、CommonDbConnector.java:1120-1184(另核对 executeCommandV2:1438-1448、JdbcContext:235-279)、SqlExecuteCommandFunction.java(全文 95 行)、MysqlConnector.java:1220-1258,1260-1310、MysqlConnectorTest.java:46-74;并用全仓 grep 确认了参数名、调用链与覆盖范围。

仲裁结论

两位 reviewer 的 PASS 结论我接受:改动方向正确(超时只在该语句新建的 Statement 上、> 0 才设置、try-with-resources 保证释放、MySQL 的 setFetchSize(Integer.MIN_VALUE) 流式语义原样保留),并且我额外核实了覆盖面比两位 reviewer 说的更广:CommonDbConnector.executeCommand 经 this::executeCommand 被 clickhouse/doris/dws/greenplum/highgo/opengauss/postgres/starrocks/tidb/vastbase/oceanbase/mysql 等约 20 个连接器注册,SqlExecuteCommandFunction 则覆盖 hive/hive1/hudi/snowflake/databend,因此新参数在这两条主链上都能到达 setQueryTimeout;仓库内只有 DefaultSqlExecutor 实现 SqlExecutor,不存在"默认方法静默吞掉超时"的实际路径;MysqlConnector 是唯一覆盖 execute(...) 的子类且已同步改造,无子类被新增重载旁路;MysqlConfig 未设 enableQueryTimeouts=false,Connector/J 默认超时机制可用。

同时确认两位都只给了 Low,但代码里确实存在若干"静默失效"路径,属于必须写清楚但不必阻塞的问题。

已接受 findings(1 中 + 6 低)

  • 中:测试只覆盖 MysqlConnector 一条(且只断言 setQueryTimeout(8) 被调用);承载几十个连接器的主路径 CommonDbConnector.execute、DefaultSqlExecutor.execute、SqlExecuteCommandFunction 的参数解析零单测,也没有"超时后抛 SQLException / <=0 不设置超时"的断言。
  • 低:queryTimeoutSeconds 用 instanceof Number 静默回落 0,而紧邻的 batchSize 用 (int) 强转——同一份 params 两个字段策略相反,字符串型超时值会被静默忽略(防护特性静默失效)。
  • 低:((Number) v).intValue() 对超 int 的 Long(如 2147483648)回绕成负数,被 > 0 判空,超时同样静默失效。
  • 低:上述解析逻辑在 CommonDbConnector:1129 与 SqlExecuteCommandFunction:77 被复制两份。
  • 低:call(存储过程/函数)命令路径完全没有超时(MysqlConnector.call:1275/1305 与 DefaultSqlExecutor.call:133 的 CallableStatement 均未设),PR 目标在这条执行路径上不完整。
  • 低:CommonDbConnector 的三参 execute 退化为委派后,executeCommand 改调四参版本——只覆盖三参 execute 的子类(改造前唯一的扩展点)会被整体旁路(不只是丢超时)。仓库内已同步,属隐蔽的未来隐患。
  • 低:queryTimeoutSeconds 无任何 javadoc/契约说明(秒、0 与负数=不限、超时抛 SQLException),而它是暴露在 execute command params 里的公共契约。
  • 低:MySQL 流式场景缺集成验证——setQueryTimeout 只对语句生效、Connector/J 依赖独立连接 KILL QUERY,而超时同样作用于逐行 fetch 阶段,长表全扫可能被判超时;单测证明不了这点。

未采纳 / 已澄清项

  • codexcli「无问题」:结论方向我认同(无回归、旧实现仍可经默认重载调用),但它完全没有覆盖"参数解析静默失效"与"测试只打了一条桩"这两块,因此不作为"零发现"采纳。
  • claudecli「Integer overflow 会 silently bypass」:问题成立,但把它归为"用户以为设了上限"的场景不准确——真正可达的输入是 JSON 传入 Long;已按实际可达路径改写并降为低。
  • claudecli「负值按 0 处理」:不采纳为缺陷,setQueryTimeout(0) 即"无限制",与 JDBC 语义一致。
  • batchSize 的 (int) 强转在 SqlExecuteCommandFunction:76 会 ClassCastException:该行不在本次改动内,属既有问题,仅作上下文提示。
  • executeCommandV2(CommonDbConnector:1438)无超时:其函数签名根本没有 params 通道,属既有 API 限制,不计入本 PR 缺陷。

下一步

① 把两处 params 解析抽成一个 helper(兼容 String、clamp 到 Integer.MAX_VALUE、非法值打日志),供 CommonDbConnector 与 SqlExecuteCommandFunction 共用;② 给 queryTimeoutSeconds 补 javadoc(SqlExecutor 接口 + 两处实现),并把三参 execute 标 @Deprecated 说明必须覆盖四参;③ 补单测:CommonDbConnector.execute 与 DefaultSqlExecutor.execute 的"设置/不设置 setQueryTimeout"、"超时抛 SQLException 后 statement 与 connection 均被关闭"、"字符串/超 int 参数"三类;④ 决定 call 路径是否也要支持超时(支持则在 call 分支透传);⑤ 有余力补一条 MySQL IT 验证流式 + 超时确实生效。

FINAL_VERDICT: PASS

===FINDINGS===
[中] connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:70
新增测试只覆盖 MySQL 一条路径,共享实现与异常路径零测试
本 PR 改动的核心在两个被几十个连接器共享的类:CommonDbConnector.execute(经 this::executeCommand 被 clickhouse/doris/dws/greenplum/highgo/opengauss/postgres/starrocks/tidb/vastbase/oceanbase/mysql 等约 20 个连接器注册)和 DefaultSqlExecutor.execute(被 hive/hive1/hudi/snowflake/databend 经 SqlExecuteCommandFunction 使用)。但仓库内 connectors-common/sql-core/src/test/java/io/tapdata/common/ 下没有任何 DefaultSqlExecutorTest/SqlExecuteCommandFunctionTest,CommonDbConnectorTest 也不含 execute/executeCommand 的用例(全仓 grep queryTimeout 只命中实现代码与本条 MysqlConnector 测试)。唯一的新用例只断言 verify(statement).setQueryTimeout(8),未覆盖:①queryTimeoutSeconds <= 0 时不得调用 setQueryTimeout(防回归到"总是设置");②超时抛 SQLException 后 Statement/Connection 是否被 try-with-resources 正确关闭、connection.commit() 是否按预期未执行;③CommonDbConnector 的 instanceof Number 解析分支(字符串/超 int 值);④SqlExecuteCommandFunction 从 params 取值的端到端路径。
建议:为 CommonDbConnector.execute 与 DefaultSqlExecutor.execute 各补一组 Mockito 用例(timeout>0 设置、timeout=0/负数不设置、execute 抛 SQLException 时资源被 close),并加一条覆盖 SqlExecuteCommandFunction params 解析(含字符串值与超 int 值)的用例。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1129
queryTimeoutSeconds 与 batchSize 的入参校验策略相反,字符串型超时会被静默忽略
同一段代码里两个参数用了互相矛盾的解析方式:batchSize 是 params.get("batchSize") != null ? (int) params.get("batchSize") : 1000(类型不对直接 ClassCastException),而 queryTimeoutSeconds 是 instanceof Number ? ((Number) ...).intValue() : 0(类型不对静默变 0)。后果是:调用方传入字符串形式的超时(如 {"queryTimeoutSeconds": "30"},外部配置/前端传参很常见)时不会报错,也不会设置超时,> 0 判断永远不成立,使用者以为超时保护已生效而实际没有——对一个"防止失控 SQL"的防护特性来说,静默失效比快速失败更危险。
建议:抽一个公共 helper 统一解析(优先 Number,其次 String 走 Integer.parseInt 并捕获异常),两处(CommonDbConnector:1129、SqlExecuteCommandFunction:77)共用;非法值至少打一条 warn 日志,或按仓库既有风格 fail-fast,别让两个相邻参数走两套语义。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1130
intValue() 对超 int 的数值回绕成负数,超时被静默关闭
((Number) params.get("queryTimeoutSeconds")).intValue() 在值为 Long 2147483648L、9999999999L 或大 BigDecimal 时按 32 位截断成负数(如 -2147483648),随后 if (queryTimeoutSeconds > 0) 判定失败,setQueryTimeout 根本不会被调用。params 一般经 JSON 反序列化,超过 Integer.MAX_VALUE 的整数会自然变成 Long,因此这不是纯理论输入。结果是用户以为设了一个"很大但仍有限"的超时,实际得到的是"无限"。
建议:解析后 Math.min(value, Integer.MAX_VALUE)(对 long 先比较再收窄),或在 helper 里显式拒绝超范围值并回报错误;并在 javadoc 里写明取值范围。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:77
同一段参数解析逻辑被复制到第二处,修正时容易漏改
SqlExecuteCommandFunction 里又写了一遍与 CommonDbConnector:1129-1130 完全相同的两行(instanceof Number + intValue()),同样的类型静默回落与溢出问题在这里也成立。两份拷贝意味着后续修一处漏一处,而这个类正是 hive/hive1/hudi/snowflake/databend 五个连接器执行 SQL 的唯一入口,行为必须与 SQL 系连接器保持一致。
建议:把解析逻辑抽成 SqlParamsUtils.getQueryTimeoutSeconds(Map)(或放进 CommonDbConnector 的静态工具方法)两处共用;同时把紧邻的 batchSize(:76,(int) 强转,Long 输入会 ClassCastException)一并收敛到同一 helper。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:79
call(存储过程/函数)路径完全没有超时,PR 目标在这条执行路径上不完整
同一个 executeCommand 的 switch 里,execute/executeQuery 分支现在会透传 queryTimeoutSeconds,但 case "call"(:83)依旧只走 sqlExecutor.call(funcName, callParams, connectionSupplier);对应实现 DefaultSqlExecutor.call:133 与 MysqlConnector.call:1275/1305 的 CallableStatement 都没有 setQueryTimeout。也就是说一条卡死的存储过程仍然可以无限期挂住任务——而这正是"支持 SQL 查询超时"最需要防护的场景之一。同时 SqlExecutor 的 call 方法签名也没有超时参数,能力缺口在接口层就已存在。
建议:至少在本 PR 说明里明确"call 路径暂不支持超时";若要做,给 SqlExecutor.call 增加带 queryTimeoutSeconds 的重载(或新增 default 方法保持兼容),在 CallableStatement 创建后 setQueryTimeout,并同步 MysqlConnector.call 的两个 CallableStatement 分支。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1147
三参 execute 退化为纯委派,只覆盖三参版本的外部子类会被静默旁路
改造后 executeCommand(:1131)调用四参 execute,而三参版本(:1147)只是 execute(sql, consumer, batchSize, 0) 的转发。在本 PR 之前,三参 execute 是唯一的扩展点:子类覆盖它即可替换整段执行逻辑。现在任何仍只覆盖三参版本的子类,executeCommand 的虚分派会直接落到 CommonDbConnector 的四参实现上,子类的自定义执行逻辑(自定义 fetchSize、自定义行转换等)被整体跳过,且没有任何编译期或运行期提示。仓库内只有 MysqlConnector:1221/1226 覆盖此方法且已同步改造成对,所以当前无实际故障,但这是个隐蔽的破坏性扩展点变更。
建议:把三参 execute 标 @Deprecated 并在 javadoc 写明"子类必须覆盖四参版本,三参版本仅为兼容保留";或直接删除三参重载(仓库内已无独立调用者),避免后来者继续按旧方式扩展。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:17
新增的 default 重载会静默丢弃超时,且接口层没有任何契约说明
SqlExecutor 新增的六参 execute 以 default 方法实现为"直接转调五参版本",因此任何未覆盖六参版本的实现都会静默忽略 queryTimeoutSeconds——没有异常、没有日志。当前仓库内只有 DefaultSqlExecutor 实现该接口且已覆盖六参,所以现在安全,但这是一个面向 connector 模块的公开接口,未来新增实现(或外部 PDK 连接器)极易踩中"设了超时却不生效"。此外 queryTimeoutSeconds 的语义(单位秒、0 与负数表示不限、超时以 SQLException 抛出)在任何注释里都没有出现,只有实现里的 > 0 暗示。
建议:在 SqlExecutor 的六参方法上补 javadoc(单位、<=0 语义、超时异常类型、default 实现忽略该参数的事实);DefaultSqlExecutor:45 的五参重载也加一行说明"转发到六参并传 0"。

[低] connectors/mysql-connector/src/main/java/io/tapdata/connector/mysql/MysqlConnector.java:1230
MySQL 流式读取下的超时语义未经验证,且超时同样会作用在逐行 fetch 阶段
execute 保留了 setFetchSize(Integer.MIN_VALUE) 的流式语义(:1232),这很好;但 setQueryTimeout 在 Connector/J 中的实现是借独立连接下发 KILL QUERY,而流式结果集的"语句执行期"包含逐行 fetch,因此一张大表的长时间流式扫描也可能在 queryTimeoutSeconds 到点后被中断——这对"防止慢 SQL"是期望行为,对"大表导出"则是行为变更,需要明确语义而不是靠用户踩坑。现有证据只有 Mockito 桩(verify(statement).setQueryTimeout(8)),既证明不了真实 MySQL 上超时确实生效,也证明不了流式 fetch 的实际表现;MysqlConnectorTest 也没有覆盖"超时异常后 connection.commit() 未执行"的路径。
建议:补一条基于真实 MySQL 的 IT(SELECT SLEEP(10) + queryTimeoutSeconds=1 应抛 SQLException),并在一条大表流式读取的 IT 上确认超时被触发时的连接清理;同时在 execute 上注释"超时同时覆盖 fetch 阶段,大表全扫请设为 0 或足够大"。

===END===
· 76251 tok · in 69600 (59648 cached / 9952 new) · out 6651 (2736 reasoning) · ¥0.0244

标记:✅ 可通行


connector.execute("select created_at from records", batches::add, 10, 8);

verify(statement).setQueryTimeout(8);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 新增测试只覆盖 MySQL 一条路径,共享实现与异常路径零测试

本 PR 改动的核心在两个被几十个连接器共享的类:CommonDbConnector.execute(经 this::executeCommand 被 clickhouse/doris/dws/greenplum/highgo/opengauss/postgres/starrocks/tidb/vastbase/oceanbase/mysql 等约 20 个连接器注册)和 DefaultSqlExecutor.execute(被 hive/hive1/hudi/snowflake/databend 经 SqlExecuteCommandFunction 使用)。但仓库内 connectors-common/sql-core/src/test/java/io/tapdata/common/ 下没有任何 DefaultSqlExecutorTest/SqlExecuteCommandFunctionTest,CommonDbConnectorTest 也不含 execute/executeCommand 的用例(全仓 grep queryTimeout 只命中实现代码与本条 MysqlConnector 测试)。唯一的新用例只断言 verify(statement).setQueryTimeout(8),未覆盖:①queryTimeoutSeconds <= 0 时不得调用 setQueryTimeout(防回归到"总是设置");②超时抛 SQLException 后 Statement/Connection 是否被 try-with-resources 正确关闭、connection.commit() 是否按预期未执行;③CommonDbConnector 的 instanceof Number 解析分支(字符串/超 int 值);④SqlExecuteCommandFunction 从 params 取值的端到端路径。
建议:为 CommonDbConnector.execute 与 DefaultSqlExecutor.execute 各补一组 Mockito 用例(timeout>0 设置、timeout=0/负数不设置、execute 抛 SQLException 时资源被 close),并加一条覆盖 SqlExecuteCommandFunction params 解析(含字符串值与超 int 值)的用例。

String sql = (String) params.get("sql");
int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;
execute(sql, list -> consumer.accept(new ExecuteResult().result(list)), batchSize);
int queryTimeoutSeconds = params.get("queryTimeoutSeconds") instanceof Number

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] queryTimeoutSeconds 与 batchSize 的入参校验策略相反,字符串型超时会被静默忽略

同一段代码里两个参数用了互相矛盾的解析方式:batchSize 是 params.get("batchSize") != null ? (int) params.get("batchSize") : 1000(类型不对直接 ClassCastException),而 queryTimeoutSeconds 是 instanceof Number ? ((Number) ...).intValue() : 0(类型不对静默变 0)。后果是:调用方传入字符串形式的超时(如 {"queryTimeoutSeconds": "30"},外部配置/前端传参很常见)时不会报错,也不会设置超时,> 0 判断永远不成立,使用者以为超时保护已生效而实际没有——对一个"防止失控 SQL"的防护特性来说,静默失效比快速失败更危险。
建议:抽一个公共 helper 统一解析(优先 Number,其次 String 走 Integer.parseInt 并捕获异常),两处(CommonDbConnector:1129、SqlExecuteCommandFunction:77)共用;非法值至少打一条 warn 日志,或按仓库既有风格 fail-fast,别让两个相邻参数走两套语义。

int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;
execute(sql, list -> consumer.accept(new ExecuteResult().result(list)), batchSize);
int queryTimeoutSeconds = params.get("queryTimeoutSeconds") instanceof Number
? ((Number) params.get("queryTimeoutSeconds")).intValue() : 0;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] intValue() 对超 int 的数值回绕成负数,超时被静默关闭

((Number) params.get("queryTimeoutSeconds")).intValue() 在值为 Long 2147483648L、9999999999L 或大 BigDecimal 时按 32 位截断成负数(如 -2147483648),随后 if (queryTimeoutSeconds > 0) 判定失败,setQueryTimeout 根本不会被调用。params 一般经 JSON 反序列化,超过 Integer.MAX_VALUE 的整数会自然变成 Long,因此这不是纯理论输入。结果是用户以为设了一个"很大但仍有限"的超时,实际得到的是"无限"。
建议:解析后 Math.min(value, Integer.MAX_VALUE)(对 long 先比较再收窄),或在 helper 里显式拒绝超范围值并回报错误;并在 javadoc 里写明取值范围。

String sql = (String) params.get("sql");
int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;
sqlExecutor.execute(sql, connectionSupplier, list -> executeResultConsumer.accept(new ExecuteResult().result(list)), aliveSupplier, batchSize);
int queryTimeoutSeconds = params.get("queryTimeoutSeconds") instanceof Number

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 同一段参数解析逻辑被复制到第二处,修正时容易漏改

SqlExecuteCommandFunction 里又写了一遍与 CommonDbConnector:1129-1130 完全相同的两行(instanceof Number + intValue()),同样的类型静默回落与溢出问题在这里也成立。两份拷贝意味着后续修一处漏一处,而这个类正是 hive/hive1/hudi/snowflake/databend 五个连接器执行 SQL 的唯一入口,行为必须与 SQL 系连接器保持一致。
建议:把解析逻辑抽成 SqlParamsUtils.getQueryTimeoutSeconds(Map)(或放进 CommonDbConnector 的静态工具方法)两处共用;同时把紧邻的 batchSize(:76,(int) 强转,Long 输入会 ClassCastException)一并收敛到同一 helper。

sqlExecutor.execute(sql, connectionSupplier, list -> executeResultConsumer.accept(new ExecuteResult().result(list)), aliveSupplier, batchSize);
int queryTimeoutSeconds = params.get("queryTimeoutSeconds") instanceof Number
? ((Number) params.get("queryTimeoutSeconds")).intValue() : 0;
sqlExecutor.execute(sql, connectionSupplier,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] call(存储过程/函数)路径完全没有超时,PR 目标在这条执行路径上不完整

同一个 executeCommand 的 switch 里,execute/executeQuery 分支现在会透传 queryTimeoutSeconds,但 case "call"(:83)依旧只走 sqlExecutor.call(funcName, callParams, connectionSupplier);对应实现 DefaultSqlExecutor.call:133 与 MysqlConnector.call:1275/1305 的 CallableStatement 都没有 setQueryTimeout。也就是说一条卡死的存储过程仍然可以无限期挂住任务——而这正是"支持 SQL 查询超时"最需要防护的场景之一。同时 SqlExecutor 的 call 方法签名也没有超时参数,能力缺口在接口层就已存在。
建议:至少在本 PR 说明里明确"call 路径暂不支持超时";若要做,给 SqlExecutor.call 增加带 queryTimeoutSeconds 的重载(或新增 default 方法保持兼容),在 CallableStatement 创建后 setQueryTimeout,并同步 MysqlConnector.call 的两个 CallableStatement 分支。

@@ -1142,8 +1145,15 @@ protected void executeCommand(TapConnectorContext connectorContext, TapExecuteCo
}

protected void execute(String sql, Consumer<Object> consumer, int batchSize) throws Throwable {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 三参 execute 退化为纯委派,只覆盖三参版本的外部子类会被静默旁路

改造后 executeCommand(:1131)调用四参 execute,而三参版本(:1147)只是 execute(sql, consumer, batchSize, 0) 的转发。在本 PR 之前,三参 execute 是唯一的扩展点:子类覆盖它即可替换整段执行逻辑。现在任何仍只覆盖三参版本的子类,executeCommand 的虚分派会直接落到 CommonDbConnector 的四参实现上,子类的自定义执行逻辑(自定义 fetchSize、自定义行转换等)被整体跳过,且没有任何编译期或运行期提示。仓库内只有 MysqlConnector:1221/1226 覆盖此方法且已同步改造成对,所以当前无实际故障,但这是个隐蔽的破坏性扩展点变更。
建议:把三参 execute 标 @Deprecated 并在 javadoc 写明"子类必须覆盖四参版本,三参版本仅为兼容保留";或直接删除三参重载(仓库内已无独立调用者),避免后来者继续按旧方式扩展。

void execute(String sql, TapSupplier<Connection> connectionSupplier, Consumer<Object> consumer, Supplier<Boolean> aliveSupplier, int batchSize) throws Throwable;

default void execute(String sql, TapSupplier<Connection> connectionSupplier, Consumer<Object> consumer,
Supplier<Boolean> aliveSupplier, int batchSize, int queryTimeoutSeconds) throws Throwable {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 新增的 default 重载会静默丢弃超时,且接口层没有任何契约说明

SqlExecutor 新增的六参 execute 以 default 方法实现为"直接转调五参版本",因此任何未覆盖六参版本的实现都会静默忽略 queryTimeoutSeconds——没有异常、没有日志。当前仓库内只有 DefaultSqlExecutor 实现该接口且已覆盖六参,所以现在安全,但这是一个面向 connector 模块的公开接口,未来新增实现(或外部 PDK 连接器)极易踩中"设了超时却不生效"。此外 queryTimeoutSeconds 的语义(单位秒、0 与负数表示不限、超时以 SQLException 抛出)在任何注释里都没有出现,只有实现里的 > 0 暗示。
建议:在 SqlExecutor 的六参方法上补 javadoc(单位、<=0 语义、超时异常类型、default 实现忽略该参数的事实);DefaultSqlExecutor:45 的五参重载也加一行说明"转发到六参并传 0"。

try (Connection connection = jdbcContext.getConnection();
Statement sqlStatement = connection.createStatement()) {
if (queryTimeoutSeconds > 0) {
sqlStatement.setQueryTimeout(queryTimeoutSeconds);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] MySQL 流式读取下的超时语义未经验证,且超时同样会作用在逐行 fetch 阶段

execute 保留了 setFetchSize(Integer.MIN_VALUE) 的流式语义(:1232),这很好;但 setQueryTimeout 在 Connector/J 中的实现是借独立连接下发 KILL QUERY,而流式结果集的"语句执行期"包含逐行 fetch,因此一张大表的长时间流式扫描也可能在 queryTimeoutSeconds 到点后被中断——这对"防止慢 SQL"是期望行为,对"大表导出"则是行为变更,需要明确语义而不是靠用户踩坑。现有证据只有 Mockito 桩(verify(statement).setQueryTimeout(8)),既证明不了真实 MySQL 上超时确实生效,也证明不了流式 fetch 的实际表现;MysqlConnectorTest 也没有覆盖"超时异常后 connection.commit() 未执行"的路径。
建议:补一条基于真实 MySQL 的 IT(SELECT SLEEP(10) + queryTimeoutSeconds=1 应抛 SQLException),并在一条大表流式读取的 IT 上确认超时被触发时的连接清理;同时在 execute 上注释"超时同时覆盖 fetch 阶段,大表全扫请设为 0 或足够大"。

@jarad0628
jarad0628 marked this pull request as ready for review October 18, 2026 13:30
@jarad0628

Copy link
Copy Markdown
Collaborator Author

AI PR Review — ✅ 可通行

  • PR: #954
  • Head: fba7757c5d79c89f2a4fd6e0116ac8f9c994be27
  • Reviewers: codexcli, claudecli, reasonix

问题清单

中

  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:18 默认 6 参 execute 静默丢弃超时,且接口层没有任何契约说明
  • connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:70 新增测试只覆盖 MySQL 一条 happy path,新解析类与两条共享执行路径零测试

低

  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1146 三参 execute 退化为纯委派,只覆盖三参版本的外部子类会被整体旁路
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:77 call(存储过程/函数)路径完全没有超时,PR 目标在这条执行路径上不完整
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlCommandParams.java:34 非正数超时被静默归一为"无限制",与相邻参数的 fail-fast 策略相反,且类无 javadoc
  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1128 batchSize 的接受域与异常类型随 helper 悄然改变,且新 helper 未校验 batchSize > 0
  • connectors/mysql-connector/src/main/java/io/tapdata/connector/mysql/MysqlConnector.java:1230 流式读取下的超时语义未经验证,且超时同样作用于逐行 fetch 阶段

最终总结

[2m ▎ thinking�[0m
· 25803 tok · in 25531 (256 cached / 25275 new) · out 272 (128 reasoning) · ¥0.0258
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && git log --oneline -5 && echo "---" && git statu...
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && ls connectors-common/sql-core/src/main/java/io/ta...
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
· 26436 tok · in 25943 (25728 cached / 215 new) · out 493 (346 reasoning) · ¥0.0017
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
�[2m ▎ thinking�[0m
· 27342 tok · in 27117 (26368 cached / 749 new) · out 225 (67 reasoning) · ¥0.0017
-> grep {"pattern": "queryTimeoutSeconds|getQueryTimeoutSeconds|setQueryTimeout|getInteger\(", "path": "/tmp/agentic-ui-pr-954-...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
· tool output truncated: 7994 of 40762 bytes elided
�[2m ▎ thinking�[0m
· 38661 tok · in 38168 (27264 cached / 10904 new) · out 493 (384 reasoning) · ¥0.0124
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> bash {"command": "echo test-probe"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
· 42643 tok · in 42408 (38656 cached / 3752 new) · out 235 (39 reasoning) · ¥0.0050
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "implements SqlExecutor|extends DefaultSqlExe...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
�[2m ▎ thinking�[0m
· 45633 tok · in 44220 (42624 cached / 1596 new) · out 1413 (1231 reasoning) · ¥0.0053
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "SqlExecuteCommandFunction"}
-> read_file {"limit": 90, "offset": 1195, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/sr...
�[2m ▎ thinking�[0m
· 48040 tok · in 47681 (45568 cached / 2113 new) · out 359 (209 reasoning) · ¥0.0037
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "void execute\(String sql, Consumer"}
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "executeCommandV2|void executeCommand\("}
�[2m ▎ thinking�[0m
· 50216 tok · in 49139 (48000 cached / 1139 new) · out 1077 (924 reasoning) · ¥0.0043
-> ls {"path": "/tmp/agentic-ui-pr-954-4826351753266312812"}
-> read_file {"limit": 70, "offset": 225, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src...
�[2m ▎ thinking�[0m
· 50991 tok · in 50793 (50176 cached / 617 new) · out 198 (37 reasoning) · ¥0.0020
-> read_file {"limit": 90, "offset": 20, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/...
-> glob {"pattern": "connectors-common/sql-core/src/test/java/io/tapdata/common/.java"}
�[2m ▎ thinking�[0m
· 55008 tok · in 52474 (50944 cached / 1530 new) · out 2534 (2447 reasoning) · ¥0.0076
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "supportExecuteCommandFunction\(this::execut...
�[2m ▎ thinking�[0m
· 57948 tok · in 56526 (54912 cached / 1614 new) · out 1422 (1260 reasoning) · ¥0.0056
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/test/java/io/tapdata/common...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "SqlCommandParams"}
�[2m ▎ thinking�[0m
· 62141 tok · in 60255 (57856 cached / 2399 new) · out 1886 (1661 reasoning) · ¥0.0073
-> review {"task": "PR #954 "TAP-12846: support SQL query timeout" at head in /tmp/agentic-ui-pr-954-4...
· 2222 tok · in 2068 (1664 cached / 404 new) · out 154 (18 reasoning) · ¥0.0007
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && git status && git log --oneline -5"}
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && git diff --stat HEAD~1 2>/dev/null || echo "no H...
· 2410 tok · in 2284 (2048 cached / 236 new) · out 126 (11 reasoning) · ¥0.0005
-> bash {"command": "git -C /tmp/agentic-ui-pr-954-4826351753266312812/checkout log --oneline -8"}
-> bash {"command": "git -C /tmp/agentic-ui-pr-954-4826351753266312812/checkout status"}
· 2728 tok · in 2583 (2304 cached / 279 new) · out 145 (74 reasoning) · ¥0.0006
-> bash {"command": "pwd"}
-> bash {"command": "git log --oneline -8"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
· 3109 tok · in 2868 (2688 cached / 180 new) · out 241 (142 reasoning) · ¥0.0007
-> ls {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout"}
-> ls {"path": "/tmp/agentic-ui-pr-954-4826351753266312812"}
· 3322 tok · in 3218 (3072 cached / 146 new) · out 104 (45 reasoning) · ¥0.0004
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/.git"}
· 3889 tok · in 3356 (3200 cached / 156 new) · out 533 (452 reasoning) · ¥0.0013
-> bash {"command": "ls /home/tapdata-connectors/.git/worktrees"}
-> glob {"pattern": "/SqlExecutor.java"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
· 4274 tok · in 4009 (3840 cached / 169 new) · out 265 (146 reasoning) · ¥0.0008
-> ls {"path": "/home/tapdata-connectors/.git/worktrees/checkout"}
-> grep {"pattern": "TAP-12846", "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout"}
· 4548 tok · in 4328 (4096 cached / 232 new) · out 220 (74 reasoning) · ¥0.0008
-> read_file {"path": "/home/tapdata-connectors/.git/worktrees/checkout/HEAD"}
-> read_file {"path": "/home/tapdata-connectors/.git/worktrees/checkout/commondir"}
-> ls {"path": "/home/tapdata-connectors/.git/worktrees/checkout/logs"}
· 4671 tok · in 4614 (4480 cached / 134 new) · out 57 · ¥0.0003
-> read_file {"path": "/home/tapdata-connectors/.git/worktrees/checkout/logs/HEAD"}
· 5154 tok · in 4746 (4608 cached / 138 new) · out 408 (256 reasoning) · ¥0.0010
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "SqlCommandParams"}
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "[Qq]ueryTimeout|setQueryTimeout|getQueryTime...
· 7658 tok · in 7091 (5120 cached / 1971 new) · out 567 (351 reasoning) · ¥0.0032
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
· 12628 tok · in 12014 (7552 cached / 4462 new) · out 614 (399 reasoning) · ¥0.0058
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
· 15018 tok · in 14350 (12544 cached / 1806 new) · out 668 (446 reasoning) · ¥0.0034
-> read_file {"limit": 70, "offset": 1205, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/sr...
-> read_file {"limit": 110, "offset": 1, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/...
· 17902 tok · in 17407 (14848 cached / 2559 new) · out 495 (374 reasoning) · ¥0.0038
-> glob {"pattern": "/tmp/
/
.patch"}
-> glob {"pattern": "/tmp//*.diff"}
-> glob {"pattern": "/tmp/agentic-ui-pr-954-4826351753266312812/
/.json"}
· 27257 tok · in 27178 (17792 cached / 9386 new) · out 79 (35 reasoning) · ¥0.0099
-> read_file {"path": "/tmp/pr-954.patch"}
· 34571 tok · in 34416 (27136 cached / 7280 new) · out 155 (106 reasoning) · ¥0.0081
-> read_file {"path": "/tmp/tap12846-query-timeout.patch"}
· 36619 tok · in 36140 (34560 cached / 1580 new) · out 479 (387 reasoning) · ¥0.0032
-> read_file {"path": "/tmp/wd.diff"}
-> read_file {"path": "/tmp/prw.diff", "limit": 80}
· 47863 tok · in 47183 (36608 cached / 10575 new) · out 680 (593 reasoning) · ¥0.0127
-> grep {"path": "/tmp", "pattern": "SqlCommandParams|MysqlConnectorTest|getQueryTimeoutSeconds", "timeout_seconds": 30}
· 55521 tok · in 48438 (47744 cached / 694 new) · out 7083 (6810 reasoning) · ¥0.0158
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors", "pattern": "protected void execute\(String s...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/main/java/io/tapdata/connec...
· 59067 tok · in 56144 (55424 cached / 720 new) · out 2923 (2742 reasoning) · ¥0.0077
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "void execute\(String sql,\s
Consumer<Objec...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "isAlive\(\)|AtomicBoolean isAlive", "timeo...
· 73953 tok · in 69445 (59008 cached / 10437 new) · out 4508 (3683 reasoning) · ¥0.0206
�[2m ▎ thinking�[0m
· 63321 tok · in 63037 (62080 cached / 957 new) · out 284 (246 reasoning) · ¥0.0028
-> ls {"path": "/tmp"}
�[2m ▎ thinking�[0m
· 70254 tok · in 70038 (63232 cached / 6806 new) · out 216 (172 reasoning) · ¥0.0085
-> read_file {"path": "/tmp/pr-954.patch"}
�[2m ▎ thinking�[0m
· 77724 tok · in 77413 (70144 cached / 7269 new) · out 311 (211 reasoning) · ¥0.0093
-> read_file {"limit": 20, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/test/java/io/t...
�[2m ▎ thinking�[0m

核对方式

本机 bash 沙箱不可用(两次尝试均返回 bash sandbox requested but unavailable on this host),因此 git diff / mvn test 都跑不了;capability 里要求执行的 test skill 因此处于 host-proven 不可用状态,本次没有真实测试结果,也没有采信任何"测试已通过"的二手日志。我改为逐文件静态核对 head 工作区(SqlExecutor.java 全文、SqlCommandParams.java 全文、CommonDbConnector.java:1120-1183、DefaultSqlExecutor.java:44-83、MysqlConnector.java:1220-1282、SqlExecuteCommandFunction.java 全文、MysqlConnectorTest.java:46-93),并用全仓 grep 核实了实现者、扩展点与调用方覆盖面。

仲裁结论

这个 head 相比上一轮实质改进了:上一轮我提的三条"静默失效"路径已被新增的 SqlCommandParams(BigDecimal + longValueExact + 显式 Integer.MAX_VALUE 上界 + 单点实现)全部修掉——instanceof Number 静默回落、intValue() 溢出回绕、两处解析拷贝,均不再成立,不再作为 finding 保留。

codexcli 的 BLOCK 我降级为「中」,理由是硬证据:全仓 implements SqlExecutor 只有 DefaultSqlExecutor(已正确覆盖 6 参,且 5 参委托 6 参传 0,无递归);SqlExecuteCommandFunction(SqlExecutor) 这个可注入构造器在仓库内没有任何调用方——hive/hive1/hudi/snowflake/databend 五个连接器走的都是静态 executeCommand(...),用的是内部 new DefaultSqlExecutor()。所以"接受了超时却永不生效"在当前仓库内没有可达路径,只是未来/外部实现(含 tapdata-connectors-enterprise,本工作区无法核对)的契约风险;把该 default 方法改成抽象反而会破坏外部实现,是更坏的兼容性选择。结论方向是"必须写进 javadoc + release note",不是"必须改设计",因此不构成阻塞。

claudecli 的 PASS 我接受,其对三条执行路径(CommonDbConnector:1153、DefaultSqlExecutor:54、MysqlConnector:1229)与 MySQL 流式保留(setFetchSize(Integer.MIN_VALUE) 在 :1232)的判断与源码一致。它和 codexcli 都漏掉的另一点我补上:CommonDbConnector.execute 的三参版本已退化为纯委派,旧扩展点被整体旁路(不只丢超时,是子类自定义执行逻辑全被跳过)——仓库内唯一覆盖者 MysqlConnector 已同步,但这是尚未被任何人指出的兼容性隐患,保留为「低」。

接受的 findings(2 中 + 5 低)

  1. 中 — SqlExecutor.java:18:6 参 default 实现静默丢弃超时,接口零契约说明(codexcli 的 block + claudecli 的两条 doc 建议合并)。
  2. 中 — MysqlConnectorTest.java:70:新增测试只有 MySQL happy path 一条;全新解析类 SqlCommandParams(含 3 条错误分支与上界校验)零测试,承载 14 个连接器的 CommonDbConnector.executeCommand 与 5 个连接器的 SqlExecuteCommandFunction 也零测试。
  3. 低 — CommonDbConnector.java:1146:三参 execute 扩展点被静默旁路(新发现,两位 reviewer 均未提)。
  4. 低 — SqlExecuteCommandFunction.java:77(call 分支在 :82):存储过程/函数路径无超时,PR 目标不完整。
  5. 低 — SqlCommandParams.java:34:非正数超时静默归一为"无限制",与相邻 batchSize 的 fail-fast 相反;类与两处解析语义均无 javadoc。
  6. 低 — CommonDbConnector.java:1128:batchSize 的接受域与异常类型悄然变化;且新 helper 未校验 batchSize > 0,而 execute 用 list.size() == batchSize 触发 flush。
  7. 低 — MysqlConnector.java:1230:流式 + 超时的真实语义未验证(Connector/J 靠独立连接 KILL QUERY,且超时覆盖逐行 fetch)。

驳回/澄清

  • codexcli「P2 应 BLOCK」:事实成立,但在仓内不可达(证据见上),且修复方式是文档而非改签名 → 降为「中」。
  • codexcli「SqlExecuteCommandFunction 注入自定义 executor 即会丢超时」:描述准确但该构造器在仓库内无调用方,故不按"当前缺陷"计。
  • claudecli「默认方法不算阻塞(标准接口演进)」:采纳,但"标准做法"不等于"可以不写 @implNote"。
  • 上一轮三条已修(instanceof Number 静默回落、intValue() 溢出、解析重复):被 SqlCommandParams 修复,不再列出。
  • "负值按 0 处理"算缺陷:不采纳,setQueryTimeout(0)=无限制,与 JDBC 语义一致;只在 finding 5 里保留"静默"这一点。
  • batchSize 的 (int) 强转 ClassCastException:已被 helper 修好,仅剩 finding 6 的行为变化说明。
  • JdbcContext.execute(sql, timeout)(:245/:268)重复实现:既有代码,不在本次 diff。
  • review 子代理声称「MysqlConnectorTest 18/18 通过」:来源是 /tmp 下无法验证的日志,拒绝采信。

下一步

① 给 SqlExecutor 6 参方法补 javadoc(秒、<=0=不限、超时抛 SQLException)+ @implNote 说明 default 实现忽略该参数,并在 PR 描述/release note 记一句;② 补测试:SqlCommandParamsTest(null/"30"/"abc"/2147483648/-1)、DefaultSqlExecutor.execute 与 CommonDbConnector.execute 各一组(>0 设置、=0/负数不设置、超时抛异常后资源关闭),并补 MysqlConnector.execute 的 0/负数用例;③ 三参 execute 标 @Deprecated 或加 javadoc 说明扩展点已迁移;④ 明确 call 路径不支持超时(或加重载);⑤ 视条件补一条真实 MySQL IT(SELECT SLEEP(10) + queryTimeoutSeconds=1)。

FINAL_VERDICT: PASS

· 83485 tok · in 78033 (77696 cached / 337 new) · out 5452 (1321 reasoning) · ¥0.0128

codexcli review
  • [P2] The timeout is silently ignored by SqlExecutor implementations that only implement the original method. The new default overload delegates to that method without passing queryTimeoutSeconds (SqlExecutor.java). Since SqlExecuteCommandFunction accepts an injected SqlExecutor, a positive timeout can be accepted but never applied on that path.

VERDICT: BLOCK

claudecli review

Looking at PR #954, I'll provide a thorough review of the SQL query timeout implementation.

Review of PR #954: SQL Query Timeout Support

Critical Analysis of Codexcli's Blocking Concern

The Default Method Silent Failure Pattern

Codexcli identified that the default method in SqlExecutor.java delegates without passing the timeout:

default void execute(..., int queryTimeoutSeconds) throws Throwable {
    execute(sql, connectionSupplier, consumer, aliveSupplier, batchSize);
}

This means custom SqlExecutor implementations that only implement the 5-parameter version will silently ignore timeout values when called via the 6-parameter version.

Assessment: This is a legitimate concern but not blocking because:

  • This is the standard Java interface evolution pattern for backward compatibility
  • All in-repo implementations (DefaultSqlExecutor, MysqlConnector via CommonDbConnector) correctly implement the 6-param version
  • The failure mode degrades to pre-existing behavior (no timeout)
  • Breaking external implementations would be worse than silent degradation of a new optional feature
  • External PDK connectors would need to deliberately inject custom SqlExecutor implementations

However: This should be documented with @implNote on the interface method.

Code Quality Review

✅ SqlCommandParams.java (New utility class)

This addresses previous reviewers' concerns about type safety:

new BigDecimal(value.toString()).intValueExact()
  • Handles Number types, String values, and other Object types uniformly
  • Throws IllegalArgumentException for invalid values (fail-fast ✓)
  • Explicitly validates against Integer.MAX_VALUE (overflow protection ✓)
  • Good separation of concerns

✅ Timeout Application

All three execution paths correctly apply timeout:

  • CommonDbConnector.execute:1153-1155
  • DefaultSqlExecutor.execute:53-55
  • MysqlConnector.execute:1229-1231

Only sets timeout when > 0, preserving existing behavior otherwise.

✅ MySQL Streaming Preserved

MysqlConnector correctly maintains setFetchSize(Integer.MIN_VALUE) streaming behavior at line 1232.

Findings

[Medium] SqlExecutor.java:18 — Default method should document silent timeout degradation

default void execute(..., int queryTimeoutSeconds) throws Throwable {
    execute(sql, connectionSupplier, consumer, aliveSupplier, batchSize);
}

The default implementation discards queryTimeoutSeconds for backward compatibility. External implementations that don't override this method will silently ignore timeouts. This is acceptable for backward compatibility but should be explicitly documented.

Recommendation: Add javadoc:

/**
 * Execute SQL with optional query timeout.
 * @param queryTimeoutSeconds maximum execution time in seconds; 0 or negative means no limit
 * @implNote The default implementation ignores the timeout parameter for backward compatibility.
 *           Implementations should override this method to support timeouts via Statement.setQueryTimeout().
 */

[Low] SqlExecutor.java:18 — No javadoc for timeout parameter semantics
The new parameter lacks documentation of:

  • Unit (seconds)
  • Behavior of 0 and negative values (no limit)
  • Exception thrown on timeout (SQLException per JDBC spec)

Recommendation: Add comprehensive javadoc as shown above.


[Low] SqlCommandParams.java:6 — Package-private utility with no class-level documentation
The class has no comment explaining its purpose or why it exists (parsing SQL command parameters safely).

Recommendation: Add class javadoc:

/**
 * Utility for parsing parameters from SQL executeCommand requests with type safety.
 */

[Low] Test coverage gaps — MysqlConnectorTest.java:46-74
The single test verifies the happy path only:

  • setQueryTimeout(8) is called ✓
  • MySQL streaming behavior preserved ✓

Not covered:

  • queryTimeoutSeconds = 0 should NOT call setQueryTimeout
  • queryTimeoutSeconds = -5 should NOT call setQueryTimeout
  • String value "30" should parse correctly (via SqlCommandParams)
  • Invalid value "abc" should throw IllegalArgumentException
  • Actual timeout exception behavior (SQLException when query exceeds timeout)
  • DefaultSqlExecutor execution path (used by Hive, Snowflake, etc.)
  • CommonDbConnector execution path (used by 20+ connectors per Reasonix's analysis)

Recommendation: Add tests for:

  1. Zero/negative timeout values (verify setQueryTimeout NOT called)
  2. SqlCommandParams.getQueryTimeoutSeconds() edge cases
  3. At least one test for DefaultSqlExecutor.execute with timeout

[Low] Stored procedure path unsupported — SqlExecuteCommandFunction.java:79-83
The case "call" branch at line 83 doesn't pass queryTimeoutSeconds to sqlExecutor.call(). The call method signature doesn't have a timeout parameter. Users calling stored procedures won't get timeout protection.

This is a known limitation based on the PR scope, but should be documented in the PR description or as a TODO comment.


Positive Observations

✅ SqlCommandParams properly handles type conversion edge cases
✅ Fail-fast on invalid parameter values
✅ Backward compatible approach (default method + overloading)
✅ All in-repo implementations correctly updated
✅ MySQL-specific behavior (streaming fetchSize) preserved
✅ Thread-safe (Statement is method-local, try-with-resources scoped)
✅ Test verifies MySQL quirks still work

Security & Correctness

  • ✅ No SQL injection risk (timeout is an int, not String concatenation)
  • ✅ No resource leaks (try-with-resources handles cleanup)
  • ✅ No race conditions (Statement not shared)
  • ✅ Fail-safe default (invalid/missing timeout → 0 → no timeout set)

Verdict

The implementation is functionally correct and safe. The default method pattern is a standard and acceptable approach for interface evolution. The SqlCommandParams utility properly addresses type safety concerns. The findings are documentation and test coverage improvements, not correctness issues.

VERDICT: PASS

reasonix review

[2m ▎ thinking�[0m
· 25803 tok · in 25531 (256 cached / 25275 new) · out 272 (128 reasoning) · ¥0.0258
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && git log --oneline -5 && echo "---" && git statu...
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && ls connectors-common/sql-core/src/main/java/io/ta...
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
· 26436 tok · in 25943 (25728 cached / 215 new) · out 493 (346 reasoning) · ¥0.0017
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
�[2m ▎ thinking�[0m
· 27342 tok · in 27117 (26368 cached / 749 new) · out 225 (67 reasoning) · ¥0.0017
-> grep {"pattern": "queryTimeoutSeconds|getQueryTimeoutSeconds|setQueryTimeout|getInteger\(", "path": "/tmp/agentic-ui-pr-954-...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
· tool output truncated: 7994 of 40762 bytes elided
�[2m ▎ thinking�[0m
· 38661 tok · in 38168 (27264 cached / 10904 new) · out 493 (384 reasoning) · ¥0.0124
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> bash {"command": "echo test-probe"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
�[2m ▎ thinking�[0m
· 42643 tok · in 42408 (38656 cached / 3752 new) · out 235 (39 reasoning) · ¥0.0050
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "implements SqlExecutor|extends DefaultSqlExe...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
�[2m ▎ thinking�[0m
· 45633 tok · in 44220 (42624 cached / 1596 new) · out 1413 (1231 reasoning) · ¥0.0053
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "SqlExecuteCommandFunction"}
-> read_file {"limit": 90, "offset": 1195, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/sr...
�[2m ▎ thinking�[0m
· 48040 tok · in 47681 (45568 cached / 2113 new) · out 359 (209 reasoning) · ¥0.0037
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "void execute\(String sql, Consumer"}
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "executeCommandV2|void executeCommand\("}
�[2m ▎ thinking�[0m
· 50216 tok · in 49139 (48000 cached / 1139 new) · out 1077 (924 reasoning) · ¥0.0043
-> ls {"path": "/tmp/agentic-ui-pr-954-4826351753266312812"}
-> read_file {"limit": 70, "offset": 225, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src...
�[2m ▎ thinking�[0m
· 50991 tok · in 50793 (50176 cached / 617 new) · out 198 (37 reasoning) · ¥0.0020
-> read_file {"limit": 90, "offset": 20, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/...
-> glob {"pattern": "connectors-common/sql-core/src/test/java/io/tapdata/common/.java"}
�[2m ▎ thinking�[0m
· 55008 tok · in 52474 (50944 cached / 1530 new) · out 2534 (2447 reasoning) · ¥0.0076
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "supportExecuteCommandFunction\(this::execut...
�[2m ▎ thinking�[0m
· 57948 tok · in 56526 (54912 cached / 1614 new) · out 1422 (1260 reasoning) · ¥0.0056
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/test/java/io/tapdata/common...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "SqlCommandParams"}
�[2m ▎ thinking�[0m
· 62141 tok · in 60255 (57856 cached / 2399 new) · out 1886 (1661 reasoning) · ¥0.0073
-> review {"task": "PR #954 "TAP-12846: support SQL query timeout" at head in /tmp/agentic-ui-pr-954-4...
· 2222 tok · in 2068 (1664 cached / 404 new) · out 154 (18 reasoning) · ¥0.0007
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && git status && git log --oneline -5"}
-> bash {"command": "cd /tmp/agentic-ui-pr-954-4826351753266312812/checkout && git diff --stat HEAD~1 2>/dev/null || echo "no H...
· 2410 tok · in 2284 (2048 cached / 236 new) · out 126 (11 reasoning) · ¥0.0005
-> bash {"command": "git -C /tmp/agentic-ui-pr-954-4826351753266312812/checkout log --oneline -8"}
-> bash {"command": "git -C /tmp/agentic-ui-pr-954-4826351753266312812/checkout status"}
· 2728 tok · in 2583 (2304 cached / 279 new) · out 145 (74 reasoning) · ¥0.0006
-> bash {"command": "pwd"}
-> bash {"command": "git log --oneline -8"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
· 3109 tok · in 2868 (2688 cached / 180 new) · out 241 (142 reasoning) · ¥0.0007
-> ls {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout"}
-> ls {"path": "/tmp/agentic-ui-pr-954-4826351753266312812"}
· 3322 tok · in 3218 (3072 cached / 146 new) · out 104 (45 reasoning) · ¥0.0004
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/.git"}
· 3889 tok · in 3356 (3200 cached / 156 new) · out 533 (452 reasoning) · ¥0.0013
-> bash {"command": "ls /home/tapdata-connectors/.git/worktrees"}
-> glob {"pattern": "/SqlExecutor.java"}
⊘ bash bash sandbox requested but unavailable on this host; refusing to run unconfined. Install bubblewrap (bwrap) or set [sandbox] bash = "off" in config.toml / Settings -> Sandbox to restore pre-1.16 unconfined shell execution.
· 4274 tok · in 4009 (3840 cached / 169 new) · out 265 (146 reasoning) · ¥0.0008
-> ls {"path": "/home/tapdata-connectors/.git/worktrees/checkout"}
-> grep {"pattern": "TAP-12846", "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout"}
· 4548 tok · in 4328 (4096 cached / 232 new) · out 220 (74 reasoning) · ¥0.0008
-> read_file {"path": "/home/tapdata-connectors/.git/worktrees/checkout/HEAD"}
-> read_file {"path": "/home/tapdata-connectors/.git/worktrees/checkout/commondir"}
-> ls {"path": "/home/tapdata-connectors/.git/worktrees/checkout/logs"}
· 4671 tok · in 4614 (4480 cached / 134 new) · out 57 · ¥0.0003
-> read_file {"path": "/home/tapdata-connectors/.git/worktrees/checkout/logs/HEAD"}
· 5154 tok · in 4746 (4608 cached / 138 new) · out 408 (256 reasoning) · ¥0.0010
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "SqlCommandParams"}
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "[Qq]ueryTimeout|setQueryTimeout|getQueryTime...
· 7658 tok · in 7091 (5120 cached / 1971 new) · out 567 (351 reasoning) · ¥0.0032
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
· 12628 tok · in 12014 (7552 cached / 4462 new) · out 614 (399 reasoning) · ¥0.0058
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> read_file {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
· 15018 tok · in 14350 (12544 cached / 1806 new) · out 668 (446 reasoning) · ¥0.0034
-> read_file {"limit": 70, "offset": 1205, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/sr...
-> read_file {"limit": 110, "offset": 1, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/...
· 17902 tok · in 17407 (14848 cached / 2559 new) · out 495 (374 reasoning) · ¥0.0038
-> glob {"pattern": "/tmp/
/
.patch"}
-> glob {"pattern": "/tmp//*.diff"}
-> glob {"pattern": "/tmp/agentic-ui-pr-954-4826351753266312812/
/.json"}
· 27257 tok · in 27178 (17792 cached / 9386 new) · out 79 (35 reasoning) · ¥0.0099
-> read_file {"path": "/tmp/pr-954.patch"}
· 34571 tok · in 34416 (27136 cached / 7280 new) · out 155 (106 reasoning) · ¥0.0081
-> read_file {"path": "/tmp/tap12846-query-timeout.patch"}
· 36619 tok · in 36140 (34560 cached / 1580 new) · out 479 (387 reasoning) · ¥0.0032
-> read_file {"path": "/tmp/wd.diff"}
-> read_file {"path": "/tmp/prw.diff", "limit": 80}
· 47863 tok · in 47183 (36608 cached / 10575 new) · out 680 (593 reasoning) · ¥0.0127
-> grep {"path": "/tmp", "pattern": "SqlCommandParams|MysqlConnectorTest|getQueryTimeoutSeconds", "timeout_seconds": 30}
· 55521 tok · in 48438 (47744 cached / 694 new) · out 7083 (6810 reasoning) · ¥0.0158
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors-common/sql-core/src/main/java/io/tapdata/common...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors", "pattern": "protected void execute\(String s...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/main/java/io/tapdata/connec...
· 59067 tok · in 56144 (55424 cached / 720 new) · out 2923 (2742 reasoning) · ¥0.0077
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "void execute\(String sql,\s
Consumer<Objec...
-> grep {"path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout", "pattern": "isAlive\(\)|AtomicBoolean isAlive", "timeo...
· 73953 tok · in 69445 (59008 cached / 10437 new) · out 4508 (3683 reasoning) · ¥0.0206
�[2m ▎ thinking�[0m
· 63321 tok · in 63037 (62080 cached / 957 new) · out 284 (246 reasoning) · ¥0.0028
-> ls {"path": "/tmp"}
�[2m ▎ thinking�[0m
· 70254 tok · in 70038 (63232 cached / 6806 new) · out 216 (172 reasoning) · ¥0.0085
-> read_file {"path": "/tmp/pr-954.patch"}
�[2m ▎ thinking�[0m
· 77724 tok · in 77413 (70144 cached / 7269 new) · out 311 (211 reasoning) · ¥0.0093
-> read_file {"limit": 20, "path": "/tmp/agentic-ui-pr-954-4826351753266312812/checkout/connectors/mysql-connector/src/test/java/io/t...
�[2m ▎ thinking�[0m

核对方式

本机 bash 沙箱不可用(两次尝试均返回 bash sandbox requested but unavailable on this host),因此 git diff / mvn test 都跑不了;capability 里要求执行的 test skill 因此处于 host-proven 不可用状态,本次没有真实测试结果,也没有采信任何"测试已通过"的二手日志。我改为逐文件静态核对 head 工作区(SqlExecutor.java 全文、SqlCommandParams.java 全文、CommonDbConnector.java:1120-1183、DefaultSqlExecutor.java:44-83、MysqlConnector.java:1220-1282、SqlExecuteCommandFunction.java 全文、MysqlConnectorTest.java:46-93),并用全仓 grep 核实了实现者、扩展点与调用方覆盖面。

仲裁结论

这个 head 相比上一轮实质改进了:上一轮我提的三条"静默失效"路径已被新增的 SqlCommandParams(BigDecimal + longValueExact + 显式 Integer.MAX_VALUE 上界 + 单点实现)全部修掉——instanceof Number 静默回落、intValue() 溢出回绕、两处解析拷贝,均不再成立,不再作为 finding 保留。

codexcli 的 BLOCK 我降级为「中」,理由是硬证据:全仓 implements SqlExecutor 只有 DefaultSqlExecutor(已正确覆盖 6 参,且 5 参委托 6 参传 0,无递归);SqlExecuteCommandFunction(SqlExecutor) 这个可注入构造器在仓库内没有任何调用方——hive/hive1/hudi/snowflake/databend 五个连接器走的都是静态 executeCommand(...),用的是内部 new DefaultSqlExecutor()。所以"接受了超时却永不生效"在当前仓库内没有可达路径,只是未来/外部实现(含 tapdata-connectors-enterprise,本工作区无法核对)的契约风险;把该 default 方法改成抽象反而会破坏外部实现,是更坏的兼容性选择。结论方向是"必须写进 javadoc + release note",不是"必须改设计",因此不构成阻塞。

claudecli 的 PASS 我接受,其对三条执行路径(CommonDbConnector:1153、DefaultSqlExecutor:54、MysqlConnector:1229)与 MySQL 流式保留(setFetchSize(Integer.MIN_VALUE) 在 :1232)的判断与源码一致。它和 codexcli 都漏掉的另一点我补上:CommonDbConnector.execute 的三参版本已退化为纯委派,旧扩展点被整体旁路(不只丢超时,是子类自定义执行逻辑全被跳过)——仓库内唯一覆盖者 MysqlConnector 已同步,但这是尚未被任何人指出的兼容性隐患,保留为「低」。

接受的 findings(2 中 + 5 低)

  1. 中 — SqlExecutor.java:18:6 参 default 实现静默丢弃超时,接口零契约说明(codexcli 的 block + claudecli 的两条 doc 建议合并)。
  2. 中 — MysqlConnectorTest.java:70:新增测试只有 MySQL happy path 一条;全新解析类 SqlCommandParams(含 3 条错误分支与上界校验)零测试,承载 14 个连接器的 CommonDbConnector.executeCommand 与 5 个连接器的 SqlExecuteCommandFunction 也零测试。
  3. 低 — CommonDbConnector.java:1146:三参 execute 扩展点被静默旁路(新发现,两位 reviewer 均未提)。
  4. 低 — SqlExecuteCommandFunction.java:77(call 分支在 :82):存储过程/函数路径无超时,PR 目标不完整。
  5. 低 — SqlCommandParams.java:34:非正数超时静默归一为"无限制",与相邻 batchSize 的 fail-fast 相反;类与两处解析语义均无 javadoc。
  6. 低 — CommonDbConnector.java:1128:batchSize 的接受域与异常类型悄然变化;且新 helper 未校验 batchSize > 0,而 execute 用 list.size() == batchSize 触发 flush。
  7. 低 — MysqlConnector.java:1230:流式 + 超时的真实语义未验证(Connector/J 靠独立连接 KILL QUERY,且超时覆盖逐行 fetch)。

驳回/澄清

  • codexcli「P2 应 BLOCK」:事实成立,但在仓内不可达(证据见上),且修复方式是文档而非改签名 → 降为「中」。
  • codexcli「SqlExecuteCommandFunction 注入自定义 executor 即会丢超时」:描述准确但该构造器在仓库内无调用方,故不按"当前缺陷"计。
  • claudecli「默认方法不算阻塞(标准接口演进)」:采纳,但"标准做法"不等于"可以不写 @implNote"。
  • 上一轮三条已修(instanceof Number 静默回落、intValue() 溢出、解析重复):被 SqlCommandParams 修复,不再列出。
  • "负值按 0 处理"算缺陷:不采纳,setQueryTimeout(0)=无限制,与 JDBC 语义一致;只在 finding 5 里保留"静默"这一点。
  • batchSize 的 (int) 强转 ClassCastException:已被 helper 修好,仅剩 finding 6 的行为变化说明。
  • JdbcContext.execute(sql, timeout)(:245/:268)重复实现:既有代码,不在本次 diff。
  • review 子代理声称「MysqlConnectorTest 18/18 通过」:来源是 /tmp 下无法验证的日志,拒绝采信。

下一步

① 给 SqlExecutor 6 参方法补 javadoc(秒、<=0=不限、超时抛 SQLException)+ @implNote 说明 default 实现忽略该参数,并在 PR 描述/release note 记一句;② 补测试:SqlCommandParamsTest(null/"30"/"abc"/2147483648/-1)、DefaultSqlExecutor.execute 与 CommonDbConnector.execute 各一组(>0 设置、=0/负数不设置、超时抛异常后资源关闭),并补 MysqlConnector.execute 的 0/负数用例;③ 三参 execute 标 @Deprecated 或加 javadoc 说明扩展点已迁移;④ 明确 call 路径不支持超时(或加重载);⑤ 视条件补一条真实 MySQL IT(SELECT SLEEP(10) + queryTimeoutSeconds=1)。

FINAL_VERDICT: PASS

===FINDINGS===
[中] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:18
默认 6 参 execute 静默丢弃超时,且接口层没有任何契约说明
default void execute(..., int queryTimeoutSeconds) 直接转调 5 参版本,任何未覆盖该重载的实现都会无异常、无日志地丢掉超时(codexcli 因此判 BLOCK)。我核实后确认当前仓库内没有可达的失效路径:全仓 implements SqlExecutor 只有 DefaultSqlExecutor,它已正确覆盖 6 参并在 :54 设置超时;SqlExecuteCommandFunction(SqlExecutor) 这个可注入构造器在仓内无任何调用方(hive/hive1/hudi/snowflake/databend 全走静态入口 + 内部 new DefaultSqlExecutor()),所以这不是现网缺陷,也不该改成抽象方法(那会破坏外部实现,比静默降级更糟)。真正的缺口是契约缺失:queryTimeoutSeconds 的单位(秒)、0/负数=不限、超时以 SQLException 抛出、以及"default 实现会忽略该参数、实现方必须覆盖"这一事实,在任何注释里都没有出现,而 SqlExecutor 是 sql-core 对外暴露的接口,tapdata-connectors-enterprise / 外部 PDK 连接器最容易踩中"设了超时却不生效"。
建议:在该重载上补 javadoc 与 @implNote("The default implementation ignores queryTimeoutSeconds for backward compatibility; implementations must override and call Statement.setQueryTimeout()"),DefaultSqlExecutor:45 的 5 参重载也加一行"转发到 6 参并传 0";同时在 PR 描述/release note 记录该兼容性约定。

[中] connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:70
新增测试只覆盖 MySQL 一条 happy path,新解析类与两条共享执行路径零测试
唯一的新用例只断言 verify(statement).setQueryTimeout(8) 与 setFetchSize(Integer.MIN_VALUE)。而本 PR 的核心在共享代码:CommonDbConnector.executeCommand/execute(14 个连接器经 supportExecuteCommandFunction(this::executeCommand) 注册:clickhouse/doris/dws/greenplum/highgo/opengauss/vastbase/oceanbase-mysql/mysql/postgres/starrocks/tidb/paimon-plus 等)与 DefaultSqlExecutor.execute(hive/hive1/hudi/snowflake/databend 经 SqlExecuteCommandFunction)。我核实 connectors-common/sql-core/src/test/java/io/tapdata/common/ 下没有 SqlCommandParamsTest、没有 DefaultSqlExecutorTest,CommonDbConnectorTest 只测索引 SQL。也就是说全新的 SqlCommandParams(两条 fail-fast 分支 + Integer.MAX_VALUE 上界分支)、<=0 不调用 setQueryTimeout 的防回归、以及"超时抛 SQLException 后 Statement/Connection 被关闭、connection.commit() 未执行"全都没有任何断言。
建议:新增 SqlCommandParamsTest(null→默认、"30"、"abc"→IAE、2147483648→IAE、-1→0、1000.5),给 DefaultSqlExecutor.execute 与 CommonDbConnector.execute 各加一组 Mockito 用例(>0 设置、=0/负数不设置 setQueryTimeout、执行抛异常时资源被 close 且未 commit),并补 MysqlConnector.execute 收到 0/负数的用例。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1146
三参 execute 退化为纯委派,只覆盖三参版本的外部子类会被整体旁路
改造后 executeCommand(:1130)虚分派到四参 execute,而三参版本(:1146)只剩 execute(sql, consumer, batchSize, 0) 的转发。在本 PR 之前,三参 execute 是唯一的扩展点:子类覆盖它即可替换整段执行逻辑(自定义 fetchSize、自定义行转换、自定义提交策略)。现在任何仍只覆盖三参版本的子类,其自定义逻辑会被完全跳过(不只是丢超时),且没有编译期或运行期提示。仓库内唯一覆盖者 MysqlConnector:1221/1226 已同步改造,所以当前无故障;但 CommonDbConnector 是 enterprise/外部连接器的基类,本工作区无法核对那些子类——这一条两位 reviewer 都没有提到。
建议:给三参 execute 标 @Deprecated 并在 javadoc 写明"子类必须覆盖四参版本,三参版本仅为兼容保留",在 release note 中提示扩展点迁移;或为三参版本保留一个可见的 hook 让旧覆盖仍然生效。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:77
call(存储过程/函数)路径完全没有超时,PR 目标在这条执行路径上不完整
同一个 executeCommand 的 switch 里,execute/executeQuery 分支已透传 queryTimeoutSeconds,但 case "call"(:82)依旧只走 sqlExecutor.call(funcName, callParams, connectionSupplier);对应实现 DefaultSqlExecutor.call:122/133 与 MysqlConnector.call:1260/1270、CommonDbConnector.call:1185/1196 的 CallableStatement 都没有 setQueryTimeout,SqlExecutor.call 的方法签名也没有超时入参——能力缺口在接口层就已存在。结果是一条卡死的存储过程仍可无限期挂住任务,而这正是"防止失控 SQL"最需要覆盖的场景之一。
建议:至少在 PR 描述里明确"call 路径暂不支持超时";若要做,给 SqlExecutor.call 增加带 queryTimeoutSeconds 的 default 重载(保持兼容),在 CallableStatement 创建后 setQueryTimeout,并同步 MysqlConnector.call 与 CommonDbConnector.call。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlCommandParams.java:34
非正数超时被静默归一为"无限制",与相邻参数的 fail-fast 策略相反,且类无 javadoc
getQueryTimeoutSeconds 对 <= 0 直接返回 0(= 不限),而同一个类里的 getInteger(:11)对非法值一律抛 IllegalArgumentException。后果是客户端把 queryTimeoutSeconds 误填 -1 时既无异常也无日志,得到的是"无限等待"——一个防护特性静默失效;对 "abc" 这类真非法值反而是 fail-fast,两种坏输入两种语义。此外新增的 final class SqlCommandParams 完全没有类级注释说明它存在的目的(统一/安全地解析 execute command params),两处调用方(CommonDbConnector:1128、SqlExecuteCommandFunction:76)的语义也无处可查;getInteger 用 intValueExact 而 getQueryTimeoutSeconds 用 longValueExact+上界比较,风格不统一(都正确)。
建议:<= 0 时按仓库日志风格打一条 debug/warn,或在 javadoc 中明确"<=0 表示不限制、负值按 0 处理";补类级 javadoc 与方法级 javadoc(单位、取值范围、抛 IllegalArgumentException 的两种情形、异常最终会以 ExecuteResult.error 返回给调用方)。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1128
batchSize 的接受域与异常类型随 helper 悄然改变,且新 helper 未校验 batchSize > 0
(int) params.get("batchSize") 换成 SqlCommandParams.getInteger(params, "batchSize", 1000) 是改进(字符串 "1000"、JSON 里变成 Long 的整数现在都能用,之前直接 ClassCastException),但它同时改变了既有参数的契约:小数(如 1000.5)从 ClassCastException 变成 IllegalArgumentException,最终以 ExecuteResult.error 呈现给调用方,属于未在 PR 描述中记录的对外行为变化。更值得顺手补的是范围校验:getInteger 不限制 batchSize > 0,而 execute 用 if (list.size() == batchSize)(:1169)触发 flush、用 batchSize > 0 ? batchSize : 1000(:1156)设 fetchSize——传入 batchSize = 0 时 fetchSize 变成 1000 但永远不会 flush,整个结果集堆积在内存里(此问题既有,但新 helper 正是加校验的最自然位置)。
建议:在 SqlCommandParams 里对 batchSize 增加 > 0 校验(或显式回落默认值并 warn),并在 PR 描述中记录"数字型字符串/Long 现在被接受、小数报错"这一行为变化。

[低] connectors/mysql-connector/src/main/java/io/tapdata/connector/mysql/MysqlConnector.java:1230
流式读取下的超时语义未经验证,且超时同样作用于逐行 fetch 阶段
execute 完整保留了 setFetchSize(Integer.MIN_VALUE) 的流式语义(:1232),这点是对的;但 Connector/J 的 setQueryTimeout 是靠独立连接下发 KILL QUERY 实现的,而流式结果集的"语句执行期"包含逐行 fetch,因此一张大表的长时间流式扫描也会在超时到点后被中断——对"防止慢 SQL"是期望行为,对"大表导出"则是行为变更,需要明确语义而不是让用户踩坑。现有证据只有 Mockito 桩(verify(statement).setQueryTimeout(8)),既证明不了真实 MySQL 上超时确实生效,也证明不了流式 fetch 时的实际表现,更没有覆盖"超时后 connection.commit() 未执行、连接被正确清理"的路径。
建议:补一条真实 MySQL IT(SELECT SLEEP(10) + queryTimeoutSeconds=1 应抛 SQLException,并断言连接被关闭),并加一条大表流式读取触发超时的用例;在 execute 上注释"超时同时覆盖 fetch 阶段,大表全扫请设为 0 或足够大"。

===END===
· 83485 tok · in 78033 (77696 cached / 337 new) · out 5452 (1321 reasoning) · ¥0.0128

标记:✅ 可通行


default void execute(String sql, TapSupplier<Connection> connectionSupplier, Consumer<Object> consumer,
Supplier<Boolean> aliveSupplier, int batchSize, int queryTimeoutSeconds) throws Throwable {
execute(sql, connectionSupplier, consumer, aliveSupplier, batchSize);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 默认 6 参 execute 静默丢弃超时,且接口层没有任何契约说明

default void execute(..., int queryTimeoutSeconds) 直接转调 5 参版本,任何未覆盖该重载的实现都会无异常、无日志地丢掉超时(codexcli 因此判 BLOCK)。我核实后确认当前仓库内没有可达的失效路径:全仓 implements SqlExecutor 只有 DefaultSqlExecutor,它已正确覆盖 6 参并在 :54 设置超时;SqlExecuteCommandFunction(SqlExecutor) 这个可注入构造器在仓内无任何调用方(hive/hive1/hudi/snowflake/databend 全走静态入口 + 内部 new DefaultSqlExecutor()),所以这不是现网缺陷,也不该改成抽象方法(那会破坏外部实现,比静默降级更糟)。真正的缺口是契约缺失:queryTimeoutSeconds 的单位(秒)、0/负数=不限、超时以 SQLException 抛出、以及"default 实现会忽略该参数、实现方必须覆盖"这一事实,在任何注释里都没有出现,而 SqlExecutor 是 sql-core 对外暴露的接口,tapdata-connectors-enterprise / 外部 PDK 连接器最容易踩中"设了超时却不生效"。
建议:在该重载上补 javadoc 与 @implNote("The default implementation ignores queryTimeoutSeconds for backward compatibility; implementations must override and call Statement.setQueryTimeout()"),DefaultSqlExecutor:45 的 5 参重载也加一行"转发到 6 参并传 0";同时在 PR 描述/release note 记录该兼容性约定。


connector.execute("select created_at from records", batches::add, 10, 8);

verify(statement).setQueryTimeout(8);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 新增测试只覆盖 MySQL 一条 happy path,新解析类与两条共享执行路径零测试

唯一的新用例只断言 verify(statement).setQueryTimeout(8) 与 setFetchSize(Integer.MIN_VALUE)。而本 PR 的核心在共享代码:CommonDbConnector.executeCommand/execute(14 个连接器经 supportExecuteCommandFunction(this::executeCommand) 注册:clickhouse/doris/dws/greenplum/highgo/opengauss/vastbase/oceanbase-mysql/mysql/postgres/starrocks/tidb/paimon-plus 等)与 DefaultSqlExecutor.execute(hive/hive1/hudi/snowflake/databend 经 SqlExecuteCommandFunction)。我核实 connectors-common/sql-core/src/test/java/io/tapdata/common/ 下没有 SqlCommandParamsTest、没有 DefaultSqlExecutorTest,CommonDbConnectorTest 只测索引 SQL。也就是说全新的 SqlCommandParams(两条 fail-fast 分支 + Integer.MAX_VALUE 上界分支)、<=0 不调用 setQueryTimeout 的防回归、以及"超时抛 SQLException 后 Statement/Connection 被关闭、connection.commit() 未执行"全都没有任何断言。
建议:新增 SqlCommandParamsTest(null→默认、"30"、"abc"→IAE、2147483648→IAE、-1→0、1000.5),给 DefaultSqlExecutor.execute 与 CommonDbConnector.execute 各加一组 Mockito 用例(>0 设置、=0/负数不设置 setQueryTimeout、执行抛异常时资源被 close 且未 commit),并补 MysqlConnector.execute 收到 0/负数的用例。

@@ -1142,8 +1144,15 @@ protected void executeCommand(TapConnectorContext connectorContext, TapExecuteCo
}

protected void execute(String sql, Consumer<Object> consumer, int batchSize) throws Throwable {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 三参 execute 退化为纯委派,只覆盖三参版本的外部子类会被整体旁路

改造后 executeCommand(:1130)虚分派到四参 execute,而三参版本(:1146)只剩 execute(sql, consumer, batchSize, 0) 的转发。在本 PR 之前,三参 execute 是唯一的扩展点:子类覆盖它即可替换整段执行逻辑(自定义 fetchSize、自定义行转换、自定义提交策略)。现在任何仍只覆盖三参版本的子类,其自定义逻辑会被完全跳过(不只是丢超时),且没有编译期或运行期提示。仓库内唯一覆盖者 MysqlConnector:1221/1226 已同步改造,所以当前无故障;但 CommonDbConnector 是 enterprise/外部连接器的基类,本工作区无法核对那些子类——这一条两位 reviewer 都没有提到。
建议:给三参 execute 标 @Deprecated 并在 javadoc 写明"子类必须覆盖四参版本,三参版本仅为兼容保留",在 release note 中提示扩展点迁移;或为三参版本保留一个可见的 hook 让旧覆盖仍然生效。

int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;
sqlExecutor.execute(sql, connectionSupplier, list -> executeResultConsumer.accept(new ExecuteResult().result(list)), aliveSupplier, batchSize);
int batchSize = SqlCommandParams.getInteger(params, "batchSize", 1000);
int queryTimeoutSeconds = SqlCommandParams.getQueryTimeoutSeconds(params);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] call(存储过程/函数)路径完全没有超时,PR 目标在这条执行路径上不完整

同一个 executeCommand 的 switch 里,execute/executeQuery 分支已透传 queryTimeoutSeconds,但 case "call"(:82)依旧只走 sqlExecutor.call(funcName, callParams, connectionSupplier);对应实现 DefaultSqlExecutor.call:122/133 与 MysqlConnector.call:1260/1270、CommonDbConnector.call:1185/1196 的 CallableStatement 都没有 setQueryTimeout,SqlExecutor.call 的方法签名也没有超时入参——能力缺口在接口层就已存在。结果是一条卡死的存储过程仍可无限期挂住任务,而这正是"防止失控 SQL"最需要覆盖的场景之一。
建议:至少在 PR 描述里明确"call 路径暂不支持超时";若要做,给 SqlExecutor.call 增加带 queryTimeoutSeconds 的 default 重载(保持兼容),在 CallableStatement 创建后 setQueryTimeout,并同步 MysqlConnector.call 与 CommonDbConnector.call。

} catch (NumberFormatException | ArithmeticException e) {
throw new IllegalArgumentException("Parameter 'queryTimeoutSeconds' must be an integer", e);
}
if (seconds <= 0) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 非正数超时被静默归一为"无限制",与相邻参数的 fail-fast 策略相反,且类无 javadoc

getQueryTimeoutSeconds 对 <= 0 直接返回 0(= 不限),而同一个类里的 getInteger(:11)对非法值一律抛 IllegalArgumentException。后果是客户端把 queryTimeoutSeconds 误填 -1 时既无异常也无日志,得到的是"无限等待"——一个防护特性静默失效;对 "abc" 这类真非法值反而是 fail-fast,两种坏输入两种语义。此外新增的 final class SqlCommandParams 完全没有类级注释说明它存在的目的(统一/安全地解析 execute command params),两处调用方(CommonDbConnector:1128、SqlExecuteCommandFunction:76)的语义也无处可查;getInteger 用 intValueExact 而 getQueryTimeoutSeconds 用 longValueExact+上界比较,风格不统一(都正确)。
建议:<= 0 时按仓库日志风格打一条 debug/warn,或在 javadoc 中明确"<=0 表示不限制、负值按 0 处理";补类级 javadoc 与方法级 javadoc(单位、取值范围、抛 IllegalArgumentException 的两种情形、异常最终会以 ExecuteResult.error 返回给调用方)。

String sql = (String) params.get("sql");
int batchSize = params.get("batchSize") != null ? (int) params.get("batchSize") : 1000;
execute(sql, list -> consumer.accept(new ExecuteResult().result(list)), batchSize);
int batchSize = SqlCommandParams.getInteger(params, "batchSize", 1000);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] batchSize 的接受域与异常类型随 helper 悄然改变,且新 helper 未校验 batchSize > 0

(int) params.get("batchSize") 换成 SqlCommandParams.getInteger(params, "batchSize", 1000) 是改进(字符串 "1000"、JSON 里变成 Long 的整数现在都能用,之前直接 ClassCastException),但它同时改变了既有参数的契约:小数(如 1000.5)从 ClassCastException 变成 IllegalArgumentException,最终以 ExecuteResult.error 呈现给调用方,属于未在 PR 描述中记录的对外行为变化。更值得顺手补的是范围校验:getInteger 不限制 batchSize > 0,而 execute 用 if (list.size() == batchSize)(:1169)触发 flush、用 batchSize > 0 ? batchSize : 1000(:1156)设 fetchSize——传入 batchSize = 0 时 fetchSize 变成 1000 但永远不会 flush,整个结果集堆积在内存里(此问题既有,但新 helper 正是加校验的最自然位置)。
建议:在 SqlCommandParams 里对 batchSize 增加 > 0 校验(或显式回落默认值并 warn),并在 PR 描述中记录"数字型字符串/Long 现在被接受、小数报错"这一行为变化。

try (Connection connection = jdbcContext.getConnection();
Statement sqlStatement = connection.createStatement()) {
if (queryTimeoutSeconds > 0) {
sqlStatement.setQueryTimeout(queryTimeoutSeconds);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 流式读取下的超时语义未经验证,且超时同样作用于逐行 fetch 阶段

execute 完整保留了 setFetchSize(Integer.MIN_VALUE) 的流式语义(:1232),这点是对的;但 Connector/J 的 setQueryTimeout 是靠独立连接下发 KILL QUERY 实现的,而流式结果集的"语句执行期"包含逐行 fetch,因此一张大表的长时间流式扫描也会在超时到点后被中断——对"防止慢 SQL"是期望行为,对"大表导出"则是行为变更,需要明确语义而不是让用户踩坑。现有证据只有 Mockito 桩(verify(statement).setQueryTimeout(8)),既证明不了真实 MySQL 上超时确实生效,也证明不了流式 fetch 时的实际表现,更没有覆盖"超时后 connection.commit() 未执行、连接被正确清理"的路径。
建议:补一条真实 MySQL IT(SELECT SLEEP(10) + queryTimeoutSeconds=1 应抛 SQLException,并断言连接被关闭),并加一条大表流式读取触发超时的用例;在 execute 上注释"超时同时覆盖 fetch 阶段,大表全扫请设为 0 或足够大"。

@jarad0628

Copy link
Copy Markdown
Collaborator Author

AI PR Review — ❌ 不可通行

  • PR: #954
  • Head: a1503946f0c7cf16841d9f6b994c050817d396c2
  • Reviewers: codexcli, claudecli, reasonix

问题清单

高

  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:26 queryTimeoutSeconds 默认 300 秒,经既有 2 参重载静默作用于所有 Paimon executeQuery 调用

中

  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:25 maxResultRows 默认 1000(配合 maxScanRows 默认 1000 万)使既有结果集直接失败
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:16 新增的 6 参默认方法静默丢弃 queryTimeoutSeconds
  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonService.java:3159 新式查询默认要求键过滤,queryAllowFullScan 对旧连接取 false,可能拒绝既有查询
  • connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:68 新特性在 sql-core 侧没有任何单测,只验证了"传 8 秒会调用 setQueryTimeout"

低

  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlCommandParams.java:34 负数/0 被静默当作"无限制",与 Paimon 对同一参数的语义冲突
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlCommandParams.java:17 intValueExact 的报错信息无法说明"小数非法"
  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:37 协作式超时无法中断已阻塞的 I/O,用户容易误以为超时等于硬性终止
  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1129 新增 queryTimeoutSeconds 参数在 SQL 连接器侧没有任何文档或 spec 描述
  • connectors-common/sql-core/src/main/java/io/tapdata/common/DefaultSqlExecutor.java:54 未处理驱动不支持 setQueryTimeout 的情况,可能让整条命令失败
  • connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:48 测试名承诺"KeepsValueConversion",实际只断言转换方法被调用
  • connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_CN.md:176 新增的执行限制/超时章节只有中文,en_US 与 zh_TW 文档完全缺失

最终总结

我先核对仓库实际代码,再逐条裁定两位 reviewer 的发现。
Bash 沙箱不可用,改用只读文件工具核查。
核查已完成(bash 沙箱在本环境不可用 —— bwrap: Creating new namespace failed,因此未运行任何测试/构建,以下结论全部来自对 head 版本源码的静态核查)。

仲裁摘要

两位 reviewer 合计 23 条(codex 3 条 + claude 20 条),去重后 12 条采纳、11 条驳回。

唯一确定阻塞项:PaimonQueryLimits 把新建的 queryTimeoutSeconds 默认值设为 300 秒,而 executeQuery(sql, log) 这个既有 2 参重载走 Collections.emptyMap()(PaimonService.java:3135),于是所有既有 Paimon executeQuery 调用都被无声加上 5 分钟硬截止,超时即抛 SQLTimeoutException 且"不返回部分结果"。这是本 PR 引入的新默认值导致的真实行为回归,两位 reviewer 都指到了同一行。

其余采纳项:SqlExecutor 6 参默认方法静默丢弃 timeout(新特性承诺了但没生效,且不报错);Paimon 默认行数上限(1000/10,000,000)在既有 SELECT * 上直接失败(需确认该上限是否本 PR 引入,但当前 head 的确无条件下发);新式查询的全扫描门禁 + queryAllowFullScan 新建连接配置默认 false;新特性在 sql-core 侧零单测(SqlCommandParams 完全没有测试类)。另有 7 条低危(错误信息、跨连接器 0/负值语义冲突、setQueryTimeout 驱动可移植性、中文单语种文档、测试命名等)。

驳回的 11 条均有代码证据:@Override 其实存在(MysqlConnector:1225)、SELECT * 重复标签分支有 size()==1 前置条件、COUNT 溢出需 9.2e18 次自增而 maxScanRows 默认 10M 先拦下、生产时钟是 System::nanoTime 单调不受 NTP 影响、nanoTime 溢出 reviewer 自行撤回、elapsedMillis() 只做减法整除不会抛、测试未用别名是刻意复现客户原始 SQL 等。

下一步必须做的事

  1. 阻塞项:把 Paimon queryTimeoutSeconds 默认改为"不设超时"(absent/0 = 无限制),仅当调用方显式传值时启用;或在 2 参重载中保持旧行为。
  2. 明确 maxResultRows/maxScanRows 是否本 PR 引入;若是,给出兼容策略(默认不限制 / 更高默认 / 迁移说明与 changelog)。
  3. SqlExecutor 6 参默认实现改为抛 UnsupportedOperationException(或强制抽象),不要静默成功。
  4. 确认 queryAllowFullScan=false 对旧连接的兼容影响,并补齐 en_US / zh_TW 文档。
  5. 补测试:SqlCommandParams.getQueryTimeoutSeconds(absent/0/负数/>Integer.MAX_VALUE/小数)、SqlExecuteCommandFunction 参数透传、MysqlConnector.execute(..., 0) 不调 setQueryTimeout、Paimon 2 参重载的既有行为。

已采纳(12 条)

高(1)

  1. PaimonQueryLimits.java:26 — queryTimeoutSeconds 默认 300 秒,经 executeQuery(sql, log) 作用于所有既有调用。

中(4)
2. PaimonQueryLimits.java:25 — maxResultRows=1000 / maxScanRows=10,000,000 默认硬失败,SELECT * 超行数即报错。
3. SqlExecutor.java:16 — 6 参默认实现静默丢弃 queryTimeoutSeconds。
4. PaimonService.java:3159 — 新式查询无键过滤即拒绝,queryAllowFullScan 旧连接默认 false。
5. MysqlConnectorTest.java:68 — 新超时链路(SqlCommandParams/SqlExecuteCommandFunction/CommonDbConnector.execute)零单测。

低(7)
6. SqlCommandParams.java:34 — 负数/0 静默当"无限制",与 Paimon(0/负数报错)语义冲突。
7. SqlCommandParams.java:17 — intValueExact 报错信息不说明小数非法。
8. PaimonQueryLimits.java:37 — 协作式超时无法中断阻塞 I/O,且只在中文文档一句话带过。
9. CommonDbConnector.java:1129 — queryTimeoutSeconds 在 SQL 连接器侧无 spec/文档,运维无从发现。
10. DefaultSqlExecutor.java:54 — 依赖 setQueryTimeout,部分驱动会抛 SQLFeatureNotSupportedException,未做兼容处理。
11. MysqlConnectorTest.java:48 — 测试名承诺"KeepsValueConversion",实际只 verify 方法被调用。
12. paimon_zh_CN.md:176 — 新章节仅中文,en_US / zh_TW 完全缺失。

已驳回(11 条,含证据)

原发现 驳回理由
claudecli#4 / codex"无法指定 unlimited" positive() 接受至 Integer.MAX_VALUE,可显式关闭限制;真正问题是默认值(已并入 [高]1、[中]2)
claudecli#6 COUNT 溢出报错无上下文 Math.addExact 需 9.2e18 次自增才溢出;maxScanRows 默认 10M 先拦下,实际不可达
claudecli#9 execute 重载缺 @Override MysqlConnector.java:1225 明确有 @Override
claudecli#10 SELECT * 后跟表达式漏检重复标签 star 分支前置条件为 getSelectItems().size() == 1;SELECT *, id 走 else 分支直接抛 "only column expressions and aggregates are supported"
claudecli#11 nanoTime 计算溢出 2,147,483,647 × 1e9 = 2.1e18 < 9.2e18;reviewer 自己也标注"Not an issue"
claudecli#12 时钟回退导致超时不触发 生产路径 clock 默认 System::nanoTime(单调),可注入 clock 仅用于包内测试构造器
claudecli#13 maxGroups 无内存上限校验 文档已明示"行数/分组数限制不等于精确内存字节上限",默认值有界(100k/1000),调大是运维显式动作
claudecli#14 orderIndex 潜在 NPE jsqlparser Expression.toString() 非空,Item.label/expressionSql 构造时均非 null
claudecli#15 params=null 与 allowFullScan 合并语义微妙 无缺陷:HashMap.putIfAbsent 在该场景行为确定,params 优先级正确且已有测试覆盖
claudecli#16 测试未用别名 该用例刻意使用客户原始无别名 SQL 并断言默认标签,与文档"建议为所有表达式指定唯一别名"一致
claudecli#17 spec.json 新增字段默认 false 该字段只影响新式查询;若新式查询本身即本 PR 引入则非回归,相关风险已并入 [中]4
claudecli#20 finally 中 elapsedMillis() 可能抛错 方法体只有减法与整除,生产时钟 System::nanoTime,不会抛出

FINAL_VERDICT: BLOCK

codexcli review

Findings

  • [P2] Paimon timeout semantics differ from an optional timeout — PaimonQueryLimits.java. When queryTimeoutSeconds is omitted, Paimon applies a 300-second deadline; when it is 0 or negative, validation rejects the command. That makes existing Paimon queries time out even without an explicit timeout and prevents callers from using a non-positive value to preserve unbounded behavior.

  • [P2] Default result limit makes existing large Paimon queries fail — PaimonQueryLimits.java, PaimonQueryPlan.java. The default is 1,000 result rows, and the accumulator throws on the next row. Thus a previously successful SELECT * or projection returning more than 1,000 rows now fails unless callers know to raise the limit.

  • [P2] The compatibility overload silently drops timeouts for existing SqlExecutor implementations — SqlExecutor.java. SqlExecuteCommandFunction invokes the new six-argument method, but its default implementation delegates to the old method without applying queryTimeoutSeconds. Any existing custom executor that inherits this default accepts the parameter while executing without a timeout.

VERDICT: BLOCK

claudecli review

I'll conduct a comprehensive review of PR #954, examining all changed files for correctness, compatibility, security, and edge cases.

Critical Issues

1. [P1] SqlExecutor default method silently ignores timeout parameter

File: connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:16-18

The new 6-parameter method has a default implementation that delegates to the old method without applying the timeout:

default void execute(..., int queryTimeoutSeconds) throws Throwable {
    execute(sql, connectionSupplier, consumer, aliveSupplier, batchSize);
}

Impact: Any existing custom SqlExecutor implementation that doesn't override the new method will silently accept queryTimeoutSeconds but execute queries without any timeout. Callers will believe the timeout is active when it isn't.

Fix: The interface should either be abstract (forcing implementations to handle it), or the default should throw UnsupportedOperationException with a clear message.

2. [P1] Paimon applies 300-second timeout by default, breaking existing queries

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:22

long seconds = positive(values, "queryTimeoutSeconds", 300, Integer.MAX_VALUE);

When queryTimeoutSeconds is absent, Paimon defaults to 300 seconds. The existing 2-parameter executeQuery(String sql, Log log) passes Collections.emptyMap(), so all existing Paimon queries now have a 5-minute deadline they didn't have before.

Impact: Existing queries that legitimately take longer than 5 minutes will start failing.

Fix: For backward compatibility, the default should match SQL standard behavior where absent/0 means unlimited. Consider defaulting to Integer.MAX_VALUE or detecting the 2-arg call path.

3. [P1] Paimon maxResultRows default of 1,000 breaks existing queries

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:22

maxResultRows = (int) positive(values, "maxResultRows", 1000, Integer.MAX_VALUE);

Queries returning more than 1,000 rows will now fail with IllegalStateException. The PR summary claims to "preserve existing behavior" but this is a breaking change.

Impact: SELECT * or aggregations returning > 1,000 groups will fail for existing clients.

Fix: Either default to a much higher value (e.g., 1,000,000) or detect legacy queries (SELECT * without GROUP BY/ORDER BY) and exempt them from this limit.

4. [P1] No way to specify "unlimited" timeout or results in Paimon

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:51-59

if (value <= 0 || value > max) { throw new ArithmeticException("out of range"); }

The validation rejects zero and negative values, so there's no way to specify "no timeout" or "unlimited results." This violates SQL conventions where 0 typically means unlimited.

Fix: Treat 0 as unlimited (map to Integer.MAX_VALUE or Long.MAX_VALUE).

High Severity

5. [P2] Missing test coverage for zero/negative/absent timeout values

File: connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java

The test only verifies queryTimeoutSeconds = 8. It doesn't verify that:

  • queryTimeoutSeconds = 0 doesn't call setQueryTimeout()
  • queryTimeoutSeconds = -1 doesn't call setQueryTimeout()
  • Absent parameter doesn't call setQueryTimeout()

This is the critical contract for backward compatibility but has no test coverage.

6. [P2] Aggregate COUNT overflow throws unhelpful ArithmeticException

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:366

count = Math.addExact(count, 1);

Math.addExact throws on overflow (after 9.2 quintillion rows), but the generic ArithmeticException won't tell the user which aggregate overflowed or which table/group it was.

Fix: Catch and wrap with context: "COUNT overflow in group {key} for table {table}"

7. [P2] Paimon timeout doesn't interrupt blocked I/O

File: connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_CN.md:174

Documentation states: "超时不能强制中断已阻塞的 Paimon/文件系统 I/O" (timeout cannot interrupt blocked Paimon/filesystem I/O).

This means queryTimeoutSeconds is a cooperative timeout checked at boundaries, not a hard kill. If a single file read hangs, the timeout won't fire until that read completes.

Impact: Users may expect a 300s timeout to guarantee termination, but a hung S3 read could block indefinitely.

Fix: Document this prominently in English docs and consider adding Thread.interrupt() to propagate cancellation to I/O layers that support it.

Medium Severity

8. [P3] SqlCommandParams.getInteger allows fractional inputs to round

File: connectors-common/sql-core/src/main/java/io/tapdata/common/SqlCommandParams.java:16

return new BigDecimal(value.toString()).intValueExact();

intValueExact() throws on fractions, but the error message says "must be a 32-bit integer" which doesn't clarify that 1.5 is invalid. The error from BigDecimal("1.5").intValueExact() is ArithmeticException: Rounding necessary.

Fix: Catch and provide clearer message: "Parameter 'batchSize' must be a whole number, got: 1.5"

9. [P3] CommonDbConnector.execute overload missing @OverRide annotation

File: connectors/mysql-connector/src/main/java/io/tapdata/connector/mysql/MysqlConnector.java:1220

protected void execute(String sql, Consumer<Object> consumer, int batchSize, int queryTimeoutSeconds) throws Throwable {

Missing @Override. While it compiles, this makes refactoring error-prone (if the parent signature changes, this won't produce a compiler error).

10. [P3] PaimonQueryPlan allows duplicate result column labels in edge case

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:73-79

String label = expressionItem.getAlias() == null ? expression.toString()
        : PaimonSqlQueryParser.unquote(expressionItem.getAlias().getName());
if (!labels.add(label)) {
    throw unsupported("duplicate result label; assign unique aliases: " + label);
}

For SELECT *, labels are taken from rowType.getFieldNames() (line 69-72) without adding to the labels set, then subsequent expressions check against an empty set. So SELECT *, id FROM table where id is already in * won't be detected as duplicate.

Fix: Add field names from SELECT * to the labels set.

11. [P3] Timeout nanosecond calculation could overflow for edge case

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:26

long seconds = positive(values, "queryTimeoutSeconds", 300, Integer.MAX_VALUE);
timeoutNanos = seconds * 1_000_000_000L;

If seconds = Integer.MAX_VALUE = 2,147,483,647, then timeoutNanos = 2,147,483,647,000,000,000 which is less than Long.MAX_VALUE = 9,223,372,036,854,775,807, so this is actually safe. Not an issue, but worth noting the calculation is close to the limit (23% of Long.MAX_VALUE).

12. [P3] PaimonQueryLimits.check() doesn't handle clock going backwards

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:37-41

void check() throws SQLTimeoutException, InterruptedException {
    if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(...); }
    if (clock.getAsLong() - started >= timeoutNanos) {
        throw new SQLTimeoutException(...);
    }
}

If the system clock is adjusted backwards (NTP correction, manual change), clock.getAsLong() - started could be negative, never triggering the timeout. System.nanoTime() is monotonic and immune to this, but the code accepts any LongSupplier clock.

Fix: Use Math.max(0, clock.getAsLong() - started) or document that clock must be monotonic.

13. [P3] Missing validation that Paimon query limits fit in memory

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:22-23

maxGroups = (int) positive(values, "maxGroups", 100_000, Integer.MAX_VALUE);
maxResultRows = (int) positive(values, "maxResultRows", 1000, Integer.MAX_VALUE);

User can set maxGroups = 2,000,000,000. With 10 aggregates per group at ~100 bytes each, that's 2TB of heap. No check that maxGroups or maxResultRows are reasonable for the JVM heap size.

Recommendation: Document max practical limits or add a sanity check (e.g., maxGroups <= 10_000_000).

Low Severity / Polish

14. [P4] Inconsistent null handling in PaimonQueryPlan.orderIndex

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:139-149

for (int i = 0; i < items.size(); i++) {
    if (items.get(i).expressionSql.equalsIgnoreCase(expression.toString()) || items.get(i).label.equals(expression.toString())
            || expression instanceof Column && items.get(i).label.equals(
            PaimonSqlQueryParser.unquote(((Column) expression).getColumnName()))) { return i; }
}

If expression.toString() returns null, equalsIgnoreCase will throw NPE. JSqlParser shouldn't return null, but defensive Objects.equals() would be safer.

15. [P4] PaimonService.executeQuery swallows allowFullScan from connection config when params is null

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonService.java:3138-3139

public Object executeQuery(String sql, Map<String, Object> params, Log log) throws Exception {
    Map<String, Object> options = new java.util.HashMap<>();
    if (params != null) { options.putAll(params); }
    options.putIfAbsent("allowFullScan", config.getQueryAllowFullScan());

If params is explicitly passed as null, it's silently converted to empty map and allowFullScan from config is applied. If params is a non-null empty map, same behavior. This is correct but subtle.

16. [P4] Misleading variable name present in test

File: connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonServiceExecuteQueryIntegrationTest.java:166

"SELECT CAST(CreationTime AS DATE), COUNT(*), SUM(TotalValues), SUM(PointsAfterRedemption) "

The test doesn't use column aliases matching the subsequent assertion logic. Should use AS creation_date, AS row_count, AS total_values, AS points_after_redemption to match the assertion pattern.

Wait, looking at line 166-170, the test does check the returned map keys, but uses the default column names (function calls). The documentation example (line 148-156) shows aliased versions for SQL Server compatibility. Minor inconsistency.

17. [P4] Undocumented breaking change in spec.json

File: connectors/paimon-plus-connector/src/main/resources/spec.json:27-33

The new queryAllowFullScan boolean is added with default: false, which changes the effective behavior for existing connections created with older spec versions (they'll get false instead of the previous implicit true for legacy queries).

Impact: Minimal, since this only affects new-style queries (GROUP BY/ORDER BY), which didn't exist before this PR.

18. [P4] Chinese documentation only

File: connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_CN.md:150-196

The comprehensive documentation of the new query limits, defaults, and timeout semantics is only in Chinese. English-speaking users will have no documentation beyond the brief PR description.

19. [P4] Test name doesn't match test behavior

File: connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:47

void executeWithTimeoutKeepsMysqlStreamingAndValueConversion()

The test name says "KeepsValueConversion" but only verifies that filterTimeForMysql is called, not that the conversion is correct. It mocks convertedRow as a fixed map and doesn't verify the actual conversion logic.

20. [P4] elapsedMillis() in finally block could throw if clock is broken

File: connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonService.java:3215

} finally {
    log.info("Paimon executeQuery table={}, splits={}, projectedColumns={}, readRows={}, matchedRows={}, groups={}, elapsedMs={}",
            tableName, plan.splits().size(), projection.length, readRows, count,
            accumulator == null ? 0 : accumulator.groupCount(), limits.elapsedMillis());
}

If limits.elapsedMillis() throws (e.g., clock broke), the original exception is suppressed. Should wrap in try-catch within finally.


Summary

Critical compatibility issues:

  1. SqlExecutor default method silently ignores timeout (P1)
  2. Paimon 300s default timeout breaks existing queries (P1)
  3. Paimon 1,000-row default limit breaks existing queries (P1)
  4. No way to specify unlimited in Paimon (P1)

Design issues:
5. Missing test coverage for zero/negative timeout (P2)
6. Unhelpful aggregate overflow errors (P2)
7. Cooperative timeout doesn't interrupt I/O (P2, documented but risky)

Additional findings: 11 medium/low issues related to edge cases, error messages, documentation, and code quality.

The SqlExecutor default method issue (#1) and Paimon breaking changes (#2, #3) are deal-breakers. The PR cannot be merged without fixing backward compatibility.

VERDICT: BLOCK

reasonix review

我先核对仓库实际代码,再逐条裁定两位 reviewer 的发现。
Bash 沙箱不可用,改用只读文件工具核查。
核查已完成(bash 沙箱在本环境不可用 —— bwrap: Creating new namespace failed,因此未运行任何测试/构建,以下结论全部来自对 head 版本源码的静态核查)。

仲裁摘要

两位 reviewer 合计 23 条(codex 3 条 + claude 20 条),去重后 12 条采纳、11 条驳回。

唯一确定阻塞项:PaimonQueryLimits 把新建的 queryTimeoutSeconds 默认值设为 300 秒,而 executeQuery(sql, log) 这个既有 2 参重载走 Collections.emptyMap()(PaimonService.java:3135),于是所有既有 Paimon executeQuery 调用都被无声加上 5 分钟硬截止,超时即抛 SQLTimeoutException 且"不返回部分结果"。这是本 PR 引入的新默认值导致的真实行为回归,两位 reviewer 都指到了同一行。

其余采纳项:SqlExecutor 6 参默认方法静默丢弃 timeout(新特性承诺了但没生效,且不报错);Paimon 默认行数上限(1000/10,000,000)在既有 SELECT * 上直接失败(需确认该上限是否本 PR 引入,但当前 head 的确无条件下发);新式查询的全扫描门禁 + queryAllowFullScan 新建连接配置默认 false;新特性在 sql-core 侧零单测(SqlCommandParams 完全没有测试类)。另有 7 条低危(错误信息、跨连接器 0/负值语义冲突、setQueryTimeout 驱动可移植性、中文单语种文档、测试命名等)。

驳回的 11 条均有代码证据:@Override 其实存在(MysqlConnector:1225)、SELECT * 重复标签分支有 size()==1 前置条件、COUNT 溢出需 9.2e18 次自增而 maxScanRows 默认 10M 先拦下、生产时钟是 System::nanoTime 单调不受 NTP 影响、nanoTime 溢出 reviewer 自行撤回、elapsedMillis() 只做减法整除不会抛、测试未用别名是刻意复现客户原始 SQL 等。

下一步必须做的事

  1. 阻塞项:把 Paimon queryTimeoutSeconds 默认改为"不设超时"(absent/0 = 无限制),仅当调用方显式传值时启用;或在 2 参重载中保持旧行为。
  2. 明确 maxResultRows/maxScanRows 是否本 PR 引入;若是,给出兼容策略(默认不限制 / 更高默认 / 迁移说明与 changelog)。
  3. SqlExecutor 6 参默认实现改为抛 UnsupportedOperationException(或强制抽象),不要静默成功。
  4. 确认 queryAllowFullScan=false 对旧连接的兼容影响,并补齐 en_US / zh_TW 文档。
  5. 补测试:SqlCommandParams.getQueryTimeoutSeconds(absent/0/负数/>Integer.MAX_VALUE/小数)、SqlExecuteCommandFunction 参数透传、MysqlConnector.execute(..., 0) 不调 setQueryTimeout、Paimon 2 参重载的既有行为。

已采纳(12 条)

高(1)

  1. PaimonQueryLimits.java:26 — queryTimeoutSeconds 默认 300 秒,经 executeQuery(sql, log) 作用于所有既有调用。

中(4)
2. PaimonQueryLimits.java:25 — maxResultRows=1000 / maxScanRows=10,000,000 默认硬失败,SELECT * 超行数即报错。
3. SqlExecutor.java:16 — 6 参默认实现静默丢弃 queryTimeoutSeconds。
4. PaimonService.java:3159 — 新式查询无键过滤即拒绝,queryAllowFullScan 旧连接默认 false。
5. MysqlConnectorTest.java:68 — 新超时链路(SqlCommandParams/SqlExecuteCommandFunction/CommonDbConnector.execute)零单测。

低(7)
6. SqlCommandParams.java:34 — 负数/0 静默当"无限制",与 Paimon(0/负数报错)语义冲突。
7. SqlCommandParams.java:17 — intValueExact 报错信息不说明小数非法。
8. PaimonQueryLimits.java:37 — 协作式超时无法中断阻塞 I/O,且只在中文文档一句话带过。
9. CommonDbConnector.java:1129 — queryTimeoutSeconds 在 SQL 连接器侧无 spec/文档,运维无从发现。
10. DefaultSqlExecutor.java:54 — 依赖 setQueryTimeout,部分驱动会抛 SQLFeatureNotSupportedException,未做兼容处理。
11. MysqlConnectorTest.java:48 — 测试名承诺"KeepsValueConversion",实际只 verify 方法被调用。
12. paimon_zh_CN.md:176 — 新章节仅中文,en_US / zh_TW 完全缺失。

已驳回(11 条,含证据)

原发现 驳回理由
claudecli#4 / codex"无法指定 unlimited" positive() 接受至 Integer.MAX_VALUE,可显式关闭限制;真正问题是默认值(已并入 [高]1、[中]2)
claudecli#6 COUNT 溢出报错无上下文 Math.addExact 需 9.2e18 次自增才溢出;maxScanRows 默认 10M 先拦下,实际不可达
claudecli#9 execute 重载缺 @Override MysqlConnector.java:1225 明确有 @Override
claudecli#10 SELECT * 后跟表达式漏检重复标签 star 分支前置条件为 getSelectItems().size() == 1;SELECT *, id 走 else 分支直接抛 "only column expressions and aggregates are supported"
claudecli#11 nanoTime 计算溢出 2,147,483,647 × 1e9 = 2.1e18 < 9.2e18;reviewer 自己也标注"Not an issue"
claudecli#12 时钟回退导致超时不触发 生产路径 clock 默认 System::nanoTime(单调),可注入 clock 仅用于包内测试构造器
claudecli#13 maxGroups 无内存上限校验 文档已明示"行数/分组数限制不等于精确内存字节上限",默认值有界(100k/1000),调大是运维显式动作
claudecli#14 orderIndex 潜在 NPE jsqlparser Expression.toString() 非空,Item.label/expressionSql 构造时均非 null
claudecli#15 params=null 与 allowFullScan 合并语义微妙 无缺陷:HashMap.putIfAbsent 在该场景行为确定,params 优先级正确且已有测试覆盖
claudecli#16 测试未用别名 该用例刻意使用客户原始无别名 SQL 并断言默认标签,与文档"建议为所有表达式指定唯一别名"一致
claudecli#17 spec.json 新增字段默认 false 该字段只影响新式查询;若新式查询本身即本 PR 引入则非回归,相关风险已并入 [中]4
claudecli#20 finally 中 elapsedMillis() 可能抛错 方法体只有减法与整除,生产时钟 System::nanoTime,不会抛出

FINAL_VERDICT: BLOCK

===FINDINGS===
[高] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:26
queryTimeoutSeconds 默认 300 秒,经既有 2 参重载静默作用于所有 Paimon executeQuery 调用
本行把新增超时参数的默认值定为 300 秒,而 PaimonService.java:3135 的 executeQuery(String sql, Log log) 传入 Collections.emptyMap(),因此所有既有调用(包括历史 SELECT * 与 COUNT 包装)都会拿到 5 分钟硬截止。check() 命中后抛 SQLTimeoutException("...no partial result returned"),原本能跑完的 5 分钟以上查询从此稳定失败,且调用方无从得知默认值存在;异常信息也没有提示可用 params.queryTimeoutSeconds 调大。两位 reviewer 独立命中同一行同一行为。
建议:默认改为"不设超时"(absent 或 0 视为无限制,与 SqlCommandParams.getQueryTimeoutSeconds 一致),仅在调用方显式传正值时启用;如需保留默认值,至少在 2 参重载中保持旧行为,并在 SQLTimeoutException 中给出当前值与调大方式。

[中] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:25
maxResultRows 默认 1000(配合 maxScanRows 默认 1000 万)使既有结果集直接失败
maxResultRows 默认 1000,PaimonQueryPlan.java:256 在下一行即 throw limits.exceeded("maxResultRows", ...);PaimonService.java:3194 同理会因 maxScanRows 默认 10,000,000 抛错。这些上限由 executeQuery(sql, log) 无条件下发,docs/paimon_zh_CN.md:178 也承认"历史 SELECT *…仍应用下面的资源限制"。因此超过 1000 行的既有 SELECT */投影查询、超过 1000 组的既有聚合、以及超过 1000 万行的计数查询都会从"成功返回"变成"整次命令失败、不返回部分结果"。请确认这些上限是否由本 PR 引入:若非本 PR 引入,则本条为确认项;若是,则属未在兼容性说明中体现的破坏性变更。
建议:默认值改为不限制(或显著提高并在 changelog 与英文文档中写明破坏性变更),或仅在调用方显式传参时启用上限;文档与错误信息需说明既有调用路径同样受限。

[中] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecutor.java:16
新增的 6 参默认方法静默丢弃 queryTimeoutSeconds
默认实现只调用 5 参旧方法,queryTimeoutSeconds 被完全忽略且不报错。本仓库内唯一实现 DefaultSqlExecutor 已正确覆写,所以运行时暂时无感;但 SqlExecutor 是 sql-core 对外公开的接口,任何未覆写(或今后新写的)实现都会在主流程上"接受超时参数却不生效",调用方以为限时已生效,属于典型的静默失效契约。这与本 PR "支持 SQL 查询超时"的目标直接冲突。
建议:默认实现改为抛 UnsupportedOperationException(附明确说明),或直接把 6 参方法设为抽象方法强制实现;至少在 Javadoc 中标注"默认实现忽略超时"。

[中] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonService.java:3159
新式查询默认要求键过滤,queryAllowFullScan 对旧连接取 false,可能拒绝既有查询
!queryPlan.legacyRows() && !limits.allowFullScan && !hasPruningFilter(...) 才放行。legacyRows() 仅覆盖 star && sorts.isEmpty() && !aggregate,因此不带键过滤的普通投影(SELECT a, b FROM t)、聚合、排序查询一律抛 IllegalArgumentException。opt-in 来源是新增连接配置 queryAllowFullScan,PaimonConfig.java:108 与 spec.json:32 的默认值都是 false,旧连接(配置里没有该字段)加载后即为 false,原先可执行的这类查询会开始报错。相关说明只写在 paimon_zh_CN.md:178。若该门禁本身即本 PR 引入,则需在兼容性说明中明确。
建议:确认门禁是否本 PR 引入;若是,请给出旧连接的迁移路径(如首次命中时提示开关位置、或在 changelog 中列为破坏性变更),并补齐 en_US / zh_TW 文档。

[中] connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:68
新特性在 sql-core 侧没有任何单测,只验证了"传 8 秒会调用 setQueryTimeout"
本例是唯一覆盖超时链路的测试,且只覆盖 queryTimeoutSeconds = 8 一个点。以下关键契约全部无覆盖:SqlCommandParams.getQueryTimeoutSeconds 对 absent/0/负数/> Integer.MAX_VALUE/小数/非数字的解析与报错(该包内类没有任何测试文件,connectors-common/sql-core/src/test 下 grep setQueryTimeout|execute\( 零命中);execute(..., 0) 与省略参数时不得调用 setQueryTimeout;SqlExecuteCommandFunction:77-80 把参数透传到 6 参方法;CommonDbConnector:1129-1131 同样路径。这正是向后兼容的边界,却没有锁定。
建议:为 SqlCommandParams 增加参数化单测,为 MysqlConnector.execute(sql, consumer, batchSize) 与 (..., 0) 增加"不调用 setQueryTimeout"断言,并为 SqlExecuteCommandFunction 增加透传测试;同时增加 Paimon executeQuery(sql, log) 2 参重载在默认配置下的回归用例。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlCommandParams.java:34
负数/0 被静默当作"无限制",与 Paimon 对同一参数的语义冲突
此处 seconds <= 0 直接返回 0(无限制),即传入 -1 这类明显笔误也不会报错;而 Paimon 的 PaimonQueryLimits.positive() 对 0 和负数抛 IllegalArgumentException("Parameter 'queryTimeoutSeconds' must be an integer between 1 and ...")。同一个平台命令名 executeQuery、同一个参数名,在 SQL 连接器上"忽略",在 Paimon 上"报错",运维与上层调度很难写出通用参数。
建议:统一语义。推荐两边都接受 0 表示无限制、负数报参数错误;或至少在 Paimon 的中文/英文文档中并列说明这一差异。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlCommandParams.java:17
intValueExact 的报错信息无法说明"小数非法"
new BigDecimal("1.5").intValueExact() 抛出的 cause 是 ArithmeticException: Rounding necessary,而对外消息只有 "Parameter 'batchSize' must be a 32-bit integer",用户看到 batchSize=1.5 时无法判断是被截断还是被拒绝。
建议:先判断 stripTrailingZeros().scale() > 0 或捕获后区分消息,例如 "Parameter 'batchSize' must be a whole number, got: 1.5"。

[低] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryLimits.java:37
协作式超时无法中断已阻塞的 I/O,用户容易误以为超时等于硬性终止
check() 只在 catalog/规划/批量/行/结果等边界轮询,标题注释也写明 "cooperative at ... boundaries"。一旦某次 S3/HDFS 读或 catalog.getTable 卡住,超时不会触发,queryTimeoutSeconds 无法给出时间上界。该限制目前只写在 paimon_zh_CN.md:192 的一句中文里。
建议:在英文与繁体文档同步该限制,并在 SQLTimeoutException 的消息或参数说明中明确"协作式截止时间,不中断阻塞 I/O";如需真正取消,考虑向支持中断的 I/O 层传播 Thread.interrupt()。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1129
新增 queryTimeoutSeconds 参数在 SQL 连接器侧没有任何文档或 spec 描述
executeCommand 现在读取 params.queryTimeoutSeconds(SqlExecuteCommandFunction:77 同),但 mysql 等连接器的 docs/mysql_en_US.md、mysql_zh_CN.md、mysql_zh_TW.md 与 spec.json 中都没有该参数的说明(全仓库 grep queryTimeoutSeconds 只在 Paimon 中文文档出现),而 batchSize 同样是命令参数,用户无从发现新能力。
建议:在各 SQL 连接器文档中补充 executeQuery 命令参数表(sql/batchSize/queryTimeoutSeconds,含 0 或省略表示无限制、单位秒、驱动侧 Statement.setQueryTimeout 生效)。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/DefaultSqlExecutor.java:54
未处理驱动不支持 setQueryTimeout 的情况,可能让整条命令失败
Statement.setQueryTimeout 在 JDBC 规范中是可选的,部分国产库/代理驱动会抛 SQLFeatureNotSupportedException。当前实现未捕获,一旦调用方传了正值,整个 execute 直接失败,退化为"不能查询"而不是"无法限时"。
建议:捕获 SQLFeatureNotSupportedException 并降级为记录 warn 后继续执行(明确告知限时未生效),或在连接初始化时探测一次并缓存能力标记。

[低] connectors/mysql-connector/src/test/java/io/tapdata/connector/mysql/MysqlConnectorTest.java:48
测试名承诺"KeepsValueConversion",实际只断言转换方法被调用
用例只 verify(connector).filterTimeForMysql(resultSet, metaData) 并把 convertedRow 固定为 mock 返回值,并未验证超时路径下 MySQL 的时间类型转换仍然正确,名称与断言强度不匹配,后续重构容易误以为已有保护。
建议:要么改名(如 executeWithTimeoutUsesMysqlStreamingPath),要么补一条真实 MetaData 驱动的转换断言(例如 TIME/TIMESTAMP 列)。

[低] connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_CN.md:176
新增的执行限制/超时章节只有中文,en_US 与 zh_TW 文档完全缺失
paimon_zh_CN.md 已扩到 10.7KB 并包含"执行限制"整节(含 queryTimeoutSeconds/maxScanRows/maxGroups/maxResultRows/allowFullScan 表格),而 paimon_en_US.md(4.1KB)与 paimon_zh_TW.md(3.8KB)grep executeQuery|queryTimeout|maxResultRows 零命中,非中文用户没有任何可用说明,而这一节恰恰描述了会让查询失败的新默认值。
建议:把该章节同步到 en_US 与 zh_TW 文档,至少补齐参数表与"限制触发即整次失败、不返回部分结果"这两点。
===END===

标记:❌ 不可通行

- GROUP BY keys normalize -0.0 to 0.0 so both zeros share one group
- ORDER BY comparison collapses signed zeros, matching the documented total order
- add maxBufferedRows (default 100000) so ordered queries fail instead of buffering unlimited rows
- update en/zh_CN/zh_TW docs and add regression tests
@jarad0628
jarad0628 force-pushed the TAP-12846-query-timeout branch from 376baee to 1db6675 Compare October 21, 2026 03:22
@jarad0628

Copy link
Copy Markdown
Collaborator Author

AI PR Review — ❌ 不可通行

  • PR: #954
  • Head: 1db667577655032f7e33253d3cf8e018d4cc3a91
  • Reviewers: codexcli, claudecli, reasonix

问题清单

高

  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:344 聚合 + ORDER BY 绕过 maxBufferedRows
  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonSqlQueryParser.java:506 合法数值谓词被拒,并报成"invalid SELECT syntax"

中

  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:48 deprecated executeCommand 静默丢弃 queryTimeoutSeconds
  • connectors/mysql-connector/src/main/java/io/tapdata/connector/mysql/MysqlConnector.java:1231 MySQL 流式读的超时覆盖范围需在文档说明
  • connectors/paimon-plus-connector/src/main/resources/docs/paimon_en_US.md:129 文档对 ORDER BY 缓冲上限的描述对聚合查询不成立
  • connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonQueryPlanTest.java:197 聚合缓冲上限的测试缺口
  • connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonSqlQueryParserTest.java:106 解析器测试把误导性报错固化为期望

低

  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1152 3 参/4 参 execute 重载可能互相递归直至栈溢出
  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1158 每次正超时调用都用反射遍历类层级
  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonSqlQueryParser.java:455 字面量改动留下不可达分支
  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:474 分组浮点列的展示值依赖扫描顺序
  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:143 ORDER BY 匹配忽略列表限定符
  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:350 排序比较器每次比较都读时钟并检查中断
  • connectors/paimon-plus-connector/pom.xml:298 编译目标回退到 Java 8,但缺少 校验
  • connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_TW.md:122 "限制"章节仍称只支持写入,与新增 executeQuery 章节矛盾
  • connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_CN.md:189 三语参数表在 queryTimeoutSeconds 行不一致
  • connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonServiceExecuteQueryIntegrationTest.java:125 测试可在失败时污染 JVM 时区
  • connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonQueryPlanTest.java:247 超时测试依赖精确的时钟读取次数

最终总结

我逐条核对了两位 reviewer 的发现,并做了实证验证(mvn -o test -Dtest=PaimonQueryPlanTest,PaimonSqlQueryParserTest 共 23 个用例全部通过;另用临时 harness 复现了 M1/M2,未改动仓库文件,git status 干净)。

核对结论

  • part 1(代码):codexcli 的 P2 与 claudecli 的 M1 是同一问题,M2 为独立问题,二者均成立。实测证据:
    • maxGroups=1000000, maxBufferedRows=2 执行 GROUP BY region ORDER BY region 3 组 → 无异常返回 3 行(文档承诺被 maxBufferedRows 拦住)。
    • WHERE id > 1.5、WHERE id < 3000000000(INT 列)→ Unsupported Paimon SQL: invalid SELECT syntax,cause 为 ArithmeticException;旧实现走 PredicateBuilder.convertJavaObject,实测 1.5→1、3000000000→-1294967296(静默截断)。
    • L1–L8 全部在 head 代码中可复核(1152、1158、1231、455/469、474、143、350、pom 298 行)。
  • part 2(文档/spec/测试):两位 reviewer 均给 PASS(仅低级别项)。其 L1、L2 与 M1、M2 同源,属实;L3–L6 逐条核对属实;spec.json、Postgres 文档、timestamp()/@TempDir 等检查项确认无问题。
  • 另核对 claudecli part2 提出的"范围外"项:SqlExecuteCommandFunction 中 deprecated 4 参 executeCommand(第 48 行)仍调用 sqlExecutor.execute(sql, supplier),会静默丢弃 queryTimeoutSeconds——属实;仓库内 Databend/Snowflake/Hive 都走 aliveSupplier 重载,故影响限于外部/遗留调用方。
  • 无一项被驳回:没有发现在证据上站不住的意见。

必须做的事(合并前)

  1. 修 M1:PaimonQueryPlan.finish() 在物化聚合结果前对 !sorts.isEmpty() 施加 maxBufferedRows,并补测试(maxBufferedRows=2 + GROUP BY region ORDER BY region 3 组应报 maxBufferedRows)。
  2. 修 M2:convertLiteral 里 byteValueExact/shortValueExact/intValueExact/longValueExact 的 ArithmeticException 不应被 parse() 归入 "invalid SELECT syntax";要么精确改写比较(id > 1.5 → id >= 2),要么给出 literal not representable as INT 之类明确错误,并在三种语言文档里写明规则、更新 PaimonSqlQueryParserTest 断言消息。
  3. 顺手修:deprecated executeCommand 路径对正值超时静默忽略;MySQL 流式读的超时覆盖范围写进文档;paimon 文档"仅支持写入"与新增 executeQuery 章节自相矛盾。

已接受的全部发现(17 项)

高(阻断)

  1. M1 — 聚合 + ORDER BY 绕过 maxBufferedRows(PaimonQueryPlan.java:344;仅受 maxResultRows/maxGroups 约束,实测可超)。
  2. M2 — 合法数值谓词被拒且报成语法错误(PaimonSqlQueryParser.java:506;intValueExact 抛 ArithmeticException 被 parse() 包装成 invalid SELECT syntax)。

中
3. deprecated executeCommand 丢弃 queryTimeoutSeconds 且不报错(SqlExecuteCommandFunction.java:48)。
4. MySQL 流式结果集(fetchSize=Integer.MIN_VALUE)下 setQueryTimeout 可能不覆盖行迭代,文档却写成整条命令有界(MysqlConnector.java:1231)。
5. 文档声称 ORDER BY 受 maxBufferedRows 限制,但 GROUP BY … ORDER BY 不成立(paimon_en_US.md:129)。
6. orderedQueriesBoundBufferedRowsWhileStreamingAndAggregatesDoNot 的聚合分支用了无 ORDER BY 的查询,完全没有覆盖缺失的检查(PaimonQueryPlanTest.java:197)。
7. PaimonSqlQueryParserTest 把 M2 的误导性报错固化进断言,只检查 IllegalArgumentException 类型(PaimonSqlQueryParserTest.java:106)。

低
8. 3 参/4 参 execute 重载互为回调可能栈溢出(子类写成 execute(sql,c,b){ execute(sql,c,b,0); })(CommonDbConnector.java:1152)。
9. 每次正超时调用都反射遍历类层级判断覆盖,且 private/bridge 同名方法会被误判为覆盖(CommonDbConnector.java:1158)。
10. 字面量改动留下死代码:SignedExpression 的 -(Double) 分支、convertLiteral 的 DECIMAL 分支(PaimonSqlQueryParser.java:455)。
11. 分组浮点列展示值取决于扫描顺序(value = next,PaimonQueryPlan.java:474)。
12. ORDER BY 忽略列的表限定符(other_table.col 会匹配已选 col)(PaimonQueryPlan.java:143)。
13. 排序比较器每次比较都读时钟 + 检查中断,O(n log n) 次(PaimonQueryPlan.java:350)。
14. pom.xml 去掉重复 compiler 插件后生产字节码回到 Java 8,但无 <release>8</release> 编译期 API 校验(pom.xml:298)。
15. paimon_zh_TW.md "限制"仍写"仅支持写入、不支持读取",与新增 executeQuery 章节矛盾(三语皆需改)(paimon_zh_TW.md:122,见第 89 行)。
16. paimon_zh_CN.md 的 queryTimeoutSeconds 行缺 0..2147483647 范围,与 en_US/zh_TW 不一致(paimon_zh_CN.md:189)。
17. realPaimonSelectStarPreservesTemporalJavaTypes 在 try 之前就 setDefault + new PaimonService(...),构造抛异常会污染 JVM 时区(PaimonServiceExecuteQueryIntegrationTest.java:125)。
18. deadlineIsCheckedInsideSortAndCheckedExceptionIsPreserved 断言精确 tick 次数 1_600_000_000L,一旦节流时钟读取就误报(PaimonQueryPlanTest.java:247)。

(第 18 项计入"低",合计 18 条,前文"17 项"表述以清单为准。)

被驳回的发现

无。两位 reviewer 的每一条具体意见都在 head(1db66757)代码中复核成立;part 2 的 PASS 是对文档/spec/测试切片的判断,不构成对 M1/M2 的放行。

FINAL_VERDICT: BLOCK

codexcli (part 1/2) review
  • P2 — Aggregate ORDER BY bypasses maxBufferedRows. In PaimonQueryPlan.java, finish() copies every aggregate group into rows and sorts it, but never checks the configured buffer limit. The limit check in add() only runs for non-aggregate rows (line 275). Thus a grouped query with ORDER BY can exceed maxBufferedRows and consume memory beyond the documented cap; enforce the cap while materializing aggregate results.

VERDICT: BLOCK

claudecli (part 1/2) review

PR #954 review, part 1 of 2 (head 1db66757)

I checked these findings against the head checkout, scoped to 0879afe9..1db66757. I did not build or run tests.

Medium

M1 — Aggregate ORDER BY ignores maxBufferedRows. I agree with Codex. (PaimonQueryPlan.java:275, :336)

  • The buffer cap in add() only runs on the non-aggregate branch.
  • finish() copies every group into rows and sorts it without checking maxBufferedRows.
  • Trigger: a caller passes maxGroups=1000000. A grouped, ordered query then buffers and sorts up to 1M rows, even though the docs say ORDER BY is capped by maxBufferedRows. The defaults (100k each) hide this.
  • Fix: before materializing in finish(), fail if !sorts.isEmpty() && states.size() > maxBufferedRows. Add a test for it.

M2 — Paimon now rejects valid numeric predicates, and reports them as syntax errors. (PaimonSqlQueryParser.convertLiteral, around :500; the catch at :96-99)

  • DoubleValue now becomes a BigDecimal. For integer columns, convertLiteral then calls byteValueExact, shortValueExact, intValueExact or longValueExact.
  • Trigger: WHERE int_col > 1.5, or int_col < 3000000000 on an INT column. Both are valid SQL. They now throw ArithmeticException, which parse rewraps as "Unsupported Paimon SQL: invalid SELECT syntax".
  • Before this change they ran, though with a silently truncated literal. Rejecting is safer than a wrong predicate, but these queries now fail outright and the message misleads the user.
  • Fix: rewrite comparisons exactly (int_col > 1.5 becomes int_col >= 2; out-of-range literals become always-true or always-false). At minimum, throw a clear "literal not representable as INT" error. Add tests.

Low

L1 — Stack overflow trap in the legacy/timeout overload pair. (CommonDbConnector.java:1150-1153)

  • The base 4-arg execute(..., 0) calls this.execute(3-arg).
  • If a subclass copies MysqlConnector's pattern but overrides only the 3-arg method (execute(sql, c, b) { execute(sql, c, b, 0); }), each method calls the other until StackOverflowError.
  • The base class should call executeJdbcQuery directly when no 3-arg override exists, or the overload contract should be documented.

L2 — The override check uses reflection on every call. (CommonDbConnector.java:1157-1172)

  • It walks the class hierarchy with getDeclaredMethod on every positive-timeout call. The result never changes per class, so it could be cached in a ClassValue.
  • It is also brittle: a private or bridge method with the same signature counts as an override.

L3 — The MySQL timeout may not cover streamed reads. (MysqlConnector.java, fetch size Integer.MIN_VALUE)

  • In Connector/J, the setQueryTimeout cancel timer is tied to statement execution. With streaming result sets, the time spent iterating rows after execute() returns may not be bounded.
  • The docs say "Positive values use JDBC Statement.setQueryTimeout" as if the whole command is bounded. Either state the streaming caveat or verify it against a real server.

L4 — Dead code from the literal changes. (PaimonSqlQueryParser.java)

  • typedLiteral returns early for every DecimalType, so the DECIMAL branch in convertLiteral can never run.
  • DoubleValue no longer yields Double, so the -((Double) value) branch in literal()'s SignedExpression handling is unreachable.

L5 — The value shown for a grouped float column depends on row order. (PaimonQueryPlan.groupKey, State.add)

  • Signed zeros are normalized for the group key, but a plain grouped column stores whichever value arrived last (value = next).
  • So a group containing both -0.0 and 0.0 may display either one, depending on scan order. This is harmless for equality, but it can produce cross-endpoint diff noise.

L6 — ORDER BY ignores a column's table qualifier. (PaimonQueryPlan.orderIndex)

  • Alias matching uses only getColumnName(), so ORDER BY other_table.col matches a selected col.
  • fieldIndex rejects foreign qualifiers elsewhere, so the behaviour is inconsistent.

L7 — The sort comparator reads the clock on every comparison. (PaimonQueryPlan.finish)

  • limits.check() calls System.nanoTime() and the interrupt check inside every comparison, which is O(n log n) calls for up to 100k rows.
  • Throttling it, for example once every 1024 comparisons, would keep cancellation responsive.

L8 — pom.xml build requirement changed. (connectors/paimon-plus-connector/pom.xml)

  • Removing the duplicate compiler plugin moves production code from Java 11 bytecode back to 8. testSource/testTarget 11 now means the build needs JDK 11 or later.
  • I grepped the main sources for Java 9+ APIs (List.of/Map.of/Set.of, isBlank, strip, repeat, readAllBytes, orElseThrow(), var, Stream.toList()) and found none.
  • Without <release>8</release>, javac does not check against the JDK 8 API, so a future regression would not be caught at compile time. Consider using <release>.

Checked and found no problem

  • SqlCommandParams validation: boolean, fractional and overflowing values are rejected, and a Long batchSize is now accepted where it used to throw ClassCastException.
  • JdbcQueryExecutor closes resources on every path, and the autocommit-aware commit is correct.
  • SqlExecutor's default overload fails loudly when a positive timeout is requested.
  • Reversed-operand comparisons map to the correct inverted operators.
  • Predicate columns stay in the projection, so executeFilter still evaluates the full predicate.
  • putIfAbsent("allowFullScan", …) also replaces an explicit null value.
  • spec.json keys contain no spelling errors.

M1 and M2 should be fixed before merge. M1 is a real bypass of a documented memory cap, and M2 makes valid queries fail with a misleading error.

VERDICT: BLOCK

codexcli (part 2/2) review

No findings in part 2. The documentation and spec additions are consistent with the implementation, and I found no actionable defects in the changed test hunks. I did not run tests.

VERDICT: PASS

claudecli (part 2/2) review

PR #954 review, part 2 of 2 (head 1db66757)

This part covers the docs, the spec.json change, and tests. I read the head checkout without building it or running tests. I found nothing new that blocks the merge here. Some of the docs and tests do lock in the two Medium issues from part 1 (M1, M2), so they need to change along with those code fixes.

Low

L1. The docs promise an ORDER BY buffer cap that grouped queries ignore, and no test covers it.

  • Docs: the zh_TW (and en_US/zh_CN) docs say queries with ORDER BY buffer result rows up to maxBufferedRows. That is false for GROUP BY … ORDER BY. Only add() checks the cap, at PaimonQueryPlan.java:275, and finish() at :336 sorts every group without checking it.
  • Test: orderedQueriesBoundBufferedRowsWhileStreamingAndAggregatesDoNot only uses a grouped query with no ORDER BY, so it never reaches the missing check. Its name also describes "aggregates are not bounded" as intended behaviour.
  • Fix: with the M1 code fix, add a case that sets maxBufferedRows=2, runs GROUP BY region ORDER BY region over 3 groups, and expects maxBufferedRows in the error.

L2. PaimonSqlQueryParserTest locks in the rejection of valid numeric predicates (part 1's M2).

  • The new rejection list includes WHERE id = 1.5 and WHERE id = 2147483648, and only checks for IllegalArgumentException. The user sees "invalid SELECT syntax" for valid SQL, and this test passes on that.
  • The docs say "column vs. constant comparisons" are supported, but never mention that literals that don't fit the column type are rejected.
  • Whatever you decide for M2 (rewrite these comparisons, or give a clear "not representable as INT" error), assert the message and document the rule.

L3. The old "write-only" limitation in paimon_zh_TW.md contradicts the new section.

  • Under 限制 (Limitations), it still says the connector only supports writes and not reads ("此連接器目前僅支持寫入操作。不支持讀取"). The new executeQuery section right below documents read-only SQL queries.
  • Narrow that line to "no CDC/streaming reads", in all three languages.

L4. The three languages differ on the queryTimeoutSeconds row.

  • en_US and zh_TW give the range 0..2147483647. zh_CN leaves the range out, though it describes the cooperative checkpoints instead.
  • Minor, but the parameter tables should agree.

L5. One test can leave the JVM timezone changed.

  • realPaimonSelectStarPreservesTemporalJavaTypes calls TimeZone.setDefault(...) and new PaimonService(...) before the try. If the constructor throws, the finally never runs and the timezone stays changed for later tests in the same JVM.
  • Move both calls inside the try, as the sibling test executesCustomerDateAggregationOnRealPaimonWithProjectionAndFiltering already does.

L6. A deadline test depends on the exact number of clock reads.

  • deadlineIsCheckedInsideSortAndCheckedExceptionIsPreserved asserts ticks.get() == 1_600_000_000L, which only holds if there are exactly four check() calls.
  • If L7 from part 1 is fixed (fewer clock reads inside the sort comparator), or another checkpoint is added, this test breaks even though behaviour is still correct.
  • Asserting the SQLTimeoutException and that the comparator ran is enough.

Checked, no problem

  • spec.json: the queryAllowFullScan key is spelled the same in the schema and in all three message sets. Its default true matches PaimonConfig (!Boolean.FALSE.equals(...)), and x-index 95 doesn't collide with any other field.
  • Postgres docs: they match the code path. PostgresConnector goes through CommonDbConnector.executeCommand, which applies queryTimeoutSeconds to both execute and executeQuery. Zero or negative batchSize falls back to 1000, as SqlCommandParams.getInteger does.
  • Integration test helper: timestamp() now builds fixtures in UTC instead of with valueOf, so they no longer shift with the JVM timezone. That matches the three-timezone parameterized tests. @TempDir gives each run a fresh warehouse, so renaming the table to DRSRedemptions doesn't leak data between tests.

Outside this part's scope: the older 4-argument SqlExecuteCommandFunction.executeCommand (:39-60) still calls the plain execute(sql, supplier) and ignores a positive queryTimeoutSeconds without telling the caller. Its root cause is part 1 code, but any connector registered through that overload gets no timeout and no error.

Part 2 adds only Low items. Overall the PR stays blocked on part 1's M1 and M2, and L1/L2 should be updated together with those fixes.

VERDICT: PASS

reasonix review

我逐条核对了两位 reviewer 的发现,并做了实证验证(mvn -o test -Dtest=PaimonQueryPlanTest,PaimonSqlQueryParserTest 共 23 个用例全部通过;另用临时 harness 复现了 M1/M2,未改动仓库文件,git status 干净)。

核对结论

  • part 1(代码):codexcli 的 P2 与 claudecli 的 M1 是同一问题,M2 为独立问题,二者均成立。实测证据:
    • maxGroups=1000000, maxBufferedRows=2 执行 GROUP BY region ORDER BY region 3 组 → 无异常返回 3 行(文档承诺被 maxBufferedRows 拦住)。
    • WHERE id > 1.5、WHERE id < 3000000000(INT 列)→ Unsupported Paimon SQL: invalid SELECT syntax,cause 为 ArithmeticException;旧实现走 PredicateBuilder.convertJavaObject,实测 1.5→1、3000000000→-1294967296(静默截断)。
    • L1–L8 全部在 head 代码中可复核(1152、1158、1231、455/469、474、143、350、pom 298 行)。
  • part 2(文档/spec/测试):两位 reviewer 均给 PASS(仅低级别项)。其 L1、L2 与 M1、M2 同源,属实;L3–L6 逐条核对属实;spec.json、Postgres 文档、timestamp()/@TempDir 等检查项确认无问题。
  • 另核对 claudecli part2 提出的"范围外"项:SqlExecuteCommandFunction 中 deprecated 4 参 executeCommand(第 48 行)仍调用 sqlExecutor.execute(sql, supplier),会静默丢弃 queryTimeoutSeconds——属实;仓库内 Databend/Snowflake/Hive 都走 aliveSupplier 重载,故影响限于外部/遗留调用方。
  • 无一项被驳回:没有发现在证据上站不住的意见。

必须做的事(合并前)

  1. 修 M1:PaimonQueryPlan.finish() 在物化聚合结果前对 !sorts.isEmpty() 施加 maxBufferedRows,并补测试(maxBufferedRows=2 + GROUP BY region ORDER BY region 3 组应报 maxBufferedRows)。
  2. 修 M2:convertLiteral 里 byteValueExact/shortValueExact/intValueExact/longValueExact 的 ArithmeticException 不应被 parse() 归入 "invalid SELECT syntax";要么精确改写比较(id > 1.5 → id >= 2),要么给出 literal not representable as INT 之类明确错误,并在三种语言文档里写明规则、更新 PaimonSqlQueryParserTest 断言消息。
  3. 顺手修:deprecated executeCommand 路径对正值超时静默忽略;MySQL 流式读的超时覆盖范围写进文档;paimon 文档"仅支持写入"与新增 executeQuery 章节自相矛盾。

已接受的全部发现(17 项)

高(阻断)

  1. M1 — 聚合 + ORDER BY 绕过 maxBufferedRows(PaimonQueryPlan.java:344;仅受 maxResultRows/maxGroups 约束,实测可超)。
  2. M2 — 合法数值谓词被拒且报成语法错误(PaimonSqlQueryParser.java:506;intValueExact 抛 ArithmeticException 被 parse() 包装成 invalid SELECT syntax)。

中
3. deprecated executeCommand 丢弃 queryTimeoutSeconds 且不报错(SqlExecuteCommandFunction.java:48)。
4. MySQL 流式结果集(fetchSize=Integer.MIN_VALUE)下 setQueryTimeout 可能不覆盖行迭代,文档却写成整条命令有界(MysqlConnector.java:1231)。
5. 文档声称 ORDER BY 受 maxBufferedRows 限制,但 GROUP BY … ORDER BY 不成立(paimon_en_US.md:129)。
6. orderedQueriesBoundBufferedRowsWhileStreamingAndAggregatesDoNot 的聚合分支用了无 ORDER BY 的查询,完全没有覆盖缺失的检查(PaimonQueryPlanTest.java:197)。
7. PaimonSqlQueryParserTest 把 M2 的误导性报错固化进断言,只检查 IllegalArgumentException 类型(PaimonSqlQueryParserTest.java:106)。

低
8. 3 参/4 参 execute 重载互为回调可能栈溢出(子类写成 execute(sql,c,b){ execute(sql,c,b,0); })(CommonDbConnector.java:1152)。
9. 每次正超时调用都反射遍历类层级判断覆盖,且 private/bridge 同名方法会被误判为覆盖(CommonDbConnector.java:1158)。
10. 字面量改动留下死代码:SignedExpression 的 -(Double) 分支、convertLiteral 的 DECIMAL 分支(PaimonSqlQueryParser.java:455)。
11. 分组浮点列展示值取决于扫描顺序(value = next,PaimonQueryPlan.java:474)。
12. ORDER BY 忽略列的表限定符(other_table.col 会匹配已选 col)(PaimonQueryPlan.java:143)。
13. 排序比较器每次比较都读时钟 + 检查中断,O(n log n) 次(PaimonQueryPlan.java:350)。
14. pom.xml 去掉重复 compiler 插件后生产字节码回到 Java 8,但无 <release>8</release> 编译期 API 校验(pom.xml:298)。
15. paimon_zh_TW.md "限制"仍写"仅支持写入、不支持读取",与新增 executeQuery 章节矛盾(三语皆需改)(paimon_zh_TW.md:122,见第 89 行)。
16. paimon_zh_CN.md 的 queryTimeoutSeconds 行缺 0..2147483647 范围,与 en_US/zh_TW 不一致(paimon_zh_CN.md:189)。
17. realPaimonSelectStarPreservesTemporalJavaTypes 在 try 之前就 setDefault + new PaimonService(...),构造抛异常会污染 JVM 时区(PaimonServiceExecuteQueryIntegrationTest.java:125)。
18. deadlineIsCheckedInsideSortAndCheckedExceptionIsPreserved 断言精确 tick 次数 1_600_000_000L,一旦节流时钟读取就误报(PaimonQueryPlanTest.java:247)。

(第 18 项计入"低",合计 18 条,前文"17 项"表述以清单为准。)

被驳回的发现

无。两位 reviewer 的每一条具体意见都在 head(1db66757)代码中复核成立;part 2 的 PASS 是对文档/spec/测试切片的判断,不构成对 M1/M2 的放行。

FINAL_VERDICT: BLOCK

===FINDINGS===
[高] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:344
聚合 + ORDER BY 绕过 maxBufferedRows
finish() 的聚合分支只校验 states.size() > maxResultRows(默认 Integer.MAX_VALUE),随后把每组结果全部塞进 rows 再排序;只有非聚合的 add()(:275 附近)检查 maxBufferedRows。
实测(maxGroups=1000000, maxBufferedRows=2,SELECT region, COUNT(*) AS n FROM orders GROUP BY region ORDER BY region,3 组):无异常返回 3 行。
默认值(各 100000)掩盖了问题,但文档明确承诺"带 ORDER BY 的查询缓存上限为 maxBufferedRows",调大 maxGroups 即可突破该内存上限。
建议:在聚合物化前加 if (!sorts.isEmpty() && states.size() > limits.maxBufferedRows) throw limits.exceeded("maxBufferedRows", limits.maxBufferedRows);,并补 maxBufferedRows=2 + 3 组的回归测试。

[高] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonSqlQueryParser.java:506
合法数值谓词被拒,并报成"invalid SELECT syntax"
DoubleValue 现在变成 BigDecimal,整数列走 byteValueExact/shortValueExact/intValueExact/longValueExact。WHERE id > 1.5、WHERE id < 3000000000(INT 列)是合法 SQL,却抛 ArithmeticException,被 parse() 的 catch (Exception) 包装为 Unsupported Paimon SQL: invalid SELECT syntax(实测确认)。
旧实现走 PredicateBuilder.convertJavaObject,实测 1.5→1、3000000000→-1294967296,即静默截断。拒绝比错谓语安全,但①这是行为回归,②用户看到的是"语法错误",误导排障,③新测试把该行为固化为期望。
建议:优先精确改写比较(id > 1.5 → id >= 2;越界字面量 → 恒真/恒假);至少把 ArithmeticException 单独捕获,抛出 literal not representable as INT 类明确错误,并在文档与测试中断言该消息。

[中] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:48
deprecated executeCommand 静默丢弃 queryTimeoutSeconds
4 参 executeCommand(..., Consumer)(无 aliveSupplier)仍调用 sqlExecutor.execute(sql, connectionSupplier),参数里的 queryTimeoutSeconds 既不生效也不报错。仓库内 Databend/Snowflake/Hive 均走 aliveSupplier 重载,但外部或遗留调用方会得到"以为有超时、其实没有"的行为,这与本 PR 的"失败要显式"原则不一致。
建议:在该重载里至少读取 queryTimeoutSeconds,正值时抛 UnsupportedOperationException(或复用 timeout 感知路径),并在 README 里注明 deprecated 重载不支持超时。

[中] connectors/mysql-connector/src/main/java/io/tapdata/connector/mysql/MysqlConnector.java:1231
MySQL 流式读的超时覆盖范围需在文档说明
该路径 setFetchSize(Integer.MIN_VALUE)(Connector/J 流式结果集),而 Statement.setQueryTimeout 的计时绑定在语句执行上;execute() 返回后的逐行迭代时间可能不受该超时约束。mysql_* 文档却写成"正值通过 JDBC Statement.setQueryTimeout 生效",读起来像整条命令有界。
建议:在 mysql 三语文档注明该限制(超时仅约束语句执行阶段,流式行读取可能超出),或对真实 MySQL 验证后写明实测结论。

[中] connectors/paimon-plus-connector/src/main/resources/docs/paimon_en_US.md:129
文档对 ORDER BY 缓冲上限的描述对聚合查询不成立
三语文档(en_US:129、zh_CN:183、zh_TW:128)均称"带 ORDER BY 的查询会把结果行缓存到内存,上限为 maxBufferedRows",但 GROUP BY … ORDER BY 实际只受 maxGroups 约束(见 PaimonQueryPlan.java:344 的检查缺口)。修复 M1 后文档才成立;否则应改写为"聚合 + 排序受 maxGroups 约束"。
建议:与 M1 的代码修复一起落地,并在参数表 maxBufferedRows 行补一句"聚合查询按组缓存,受 maxGroups 约束"。

[中] connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonQueryPlanTest.java:197
聚合缓冲上限的测试缺口
orderedQueriesBoundBufferedRowsWhileStreamingAndAggregatesDoNot 的聚合分支(:197-200)用的是无 ORDER BY 的 GROUP BY,因此永远不会走到 finish() 中缺失的检查;用例名还把"聚合不受 maxBufferedRows 限制"描述成预期行为,等于把 bug 写进了约束。
建议:修复 M1 时新增用例:maxBufferedRows=2,GROUP BY region ORDER BY region 覆盖 3 个分组,断言错误消息包含 maxBufferedRows。

[中] connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonSqlQueryParserTest.java:106
解析器测试把误导性报错固化为期望
新增的拒绝列表包含 WHERE id = 1.5、WHERE id = 2147483648,只断言 IllegalArgumentException 类型,因此"合法 SQL 被报成 invalid SELECT syntax"这个缺陷不会被测试发现;文档也只说支持"列与常量比较",从未说明超范围字面量会被拒绝。
建议:随 M2 一起调整——若选择精确改写则把这些用例移出拒绝列表并断言改写后的语义;若保留拒绝,则断言消息(如 "not representable as INT")并在三语文档写明规则。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1152
3 参/4 参 execute 重载可能互相递归直至栈溢出
基类 4 参在 timeout==0 时回调 execute(sql, consumer, batchSize)(虚拟分派)。若子类按"只重写 3 参、内部委托 4 参"的写法(execute(sql,c,b){ execute(sql,c,b,0); }),而自己没有重写 4 参,就会 3 参↔基类 4 参无限递归 → StackOverflowError。当前仓库内只有 MysqlConnector 重写且两个都重写了,所以尚未触发。
建议:在 4 参的 timeout==0 分支直接用 executeJdbcQuery(...)(仅在检测到 3 参被重写时才委派),或在方法注释/README 明确"3 参重写不得回调 4 参"的契约。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1158
每次正超时调用都用反射遍历类层级
getDeclaredMethod 逐层扫描的结果对同一个类永远不变,却在每次调用时重算;并且 private 方法或 bridge 方法同名也会被记为"已重写",导致本应报 UnsupportedOperationException 的连接器静默走通用 JDBC 路径。
建议:用 ClassValue 缓存判定结果,并要求方法 modifier 非 private、非 bridge/synthetic。

[低] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonSqlQueryParser.java:455
字面量改动留下不可达分支
literal() 中 DoubleValue 已返回 BigDecimal,故 SignedExpression 里的 value instanceof Double 分支(:455)不可达;typedLiteral 对所有 DecimalType 提前返回,convertLiteral 的 DECIMAL 分支同样不可达。
建议:删除这两处死分支,或用注释标注为防御性代码并补一行覆盖说明,避免后续维护者误判类型。

[低] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:474
分组浮点列的展示值依赖扫描顺序
groupKey 已把 -0.0/+0.0 归一化到同一分组,但普通分组列的 State.add 直接 value = next(最后到达者胜出)。同一分组内同时出现 -0.0 与 0.0 时,展示哪个符号取决于扫描顺序,不影响相等性,但会造成跨端 diff 噪声。
建议:对浮点列在存储时同样归一化(如 value = groupKey(next) 或统一存 +0.0),并补一条断言。

[低] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:143
ORDER BY 匹配忽略列表限定符
orderIndex 只用 getColumnName() 做别名匹配,因此 SELECT col FROM t ORDER BY other_table.col 会静默匹配已选 col;而 fieldIndex 对其它表限定符是显式拒绝的,行为不一致。
建议:当 Column 带 table qualifier 时,先校验其等于当前表名,否则抛 unsupported("ORDER BY references another table: ..."),与 fieldIndex 保持一致。

[低] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:350
排序比较器每次比较都读时钟并检查中断
limits.check() 里含 clock.getAsLong()(超时开启时)与 Thread.interrupted 检查,被放在比较器内部,最多约 100k 行的排序会产生 O(n log n) 次系统调用级读取。
建议:按比较次数节流(例如每 1024 次比较检查一次),既保持取消响应性又显著降低开销。

[低] connectors/paimon-plus-connector/pom.xml:298
编译目标回退到 Java 8,但缺少 校验
删除重复的 compiler 插件后,生产代码从 Java 11 字节码回到 8;新增 testSource/testTarget=11 意味着构建需要 JDK 11+。当前主源码没有 Java 9+ API(已 grep List.of/isBlank/var/Stream.toList() 等),但 javac 未对 JDK 8 API 做校验,未来回归不会在编译期被发现。
建议:把 <source>/<target> 换成 <release>8</release>(测试用 testRelease 11),让编译期真正按 JDK 8 API 校验。

[低] connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_TW.md:122
"限制"章节仍称只支持写入,与新增 executeQuery 章节矛盾
第 89 行(en_US:90、zh_CN:110 同)写"此連接器目前僅支持寫入操作。不支持讀取和 CDC 操作",而紧随其后的 executeQuery 章节(:122 起)详细描述了只读 SQL 查询。
建议:把该条收窄为"不支持 CDC/流式读取(仅支持只读 ad-hoc 查询)",三语同步。

[低] connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_CN.md:189
三语参数表在 queryTimeoutSeconds 行不一致
en_US(:135)与 zh_TW(:134)写明范围 0..2147483647,zh_CN 只描述协作式检查点而缺范围。
建议:zh_CN 补齐范围与"JDBC 上界一致"的说明,使三语参数表一致。

[低] connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonServiceExecuteQueryIntegrationTest.java:125
测试可在失败时污染 JVM 时区
realPaimonSelectStarPreservesTemporalJavaTypes 在 try 之前调用 TimeZone.setDefault(...) 与 new PaimonService(...);若构造或 config() 抛异常,finally 不执行,时区会残留给同 JVM 的后续测试。兄弟用例 executesCustomerDateAggregation...(:164)已把这两步放进 try。
建议:把 setDefault 之后的 new PaimonService(...) 移入 try,或用 try/finally 包住整个方法体。

[低] connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonQueryPlanTest.java:247
超时测试依赖精确的时钟读取次数
assertEquals(1_600_000_000L, ticks.get()) 只有在恰好 4 次 check() 时才成立;一旦按建议给比较器节流(或新增检查点),行为仍正确但测试会误报。
建议:只断言抛出 SQLTimeoutException 且消息含 queryTimeoutSeconds=1,并断言比较器确实执行过(例如计数器 > 0),不绑定 tick 具体数值。
===END===

标记:❌ 不可通行

limits.check();
Object[] result = new Object[items.size()];
for (int i = 0; i < result.length; i++) { result[i] = state[i].result(); }
rows.add(result);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[高] 聚合 + ORDER BY 绕过 maxBufferedRows

finish() 的聚合分支只校验 states.size() > maxResultRows(默认 Integer.MAX_VALUE),随后把每组结果全部塞进 rows 再排序;只有非聚合的 add()(:275 附近)检查 maxBufferedRows。
实测(maxGroups=1000000, maxBufferedRows=2,SELECT region, COUNT(*) AS n FROM orders GROUP BY region ORDER BY region,3 组):无异常返回 3 行。
默认值(各 100000)掩盖了问题,但文档明确承诺"带 ORDER BY 的查询缓存上限为 maxBufferedRows",调大 maxGroups 即可突破该内存上限。
建议:在聚合物化前加 if (!sorts.isEmpty() && states.size() > limits.maxBufferedRows) throw limits.exceeded("maxBufferedRows", limits.maxBufferedRows);,并补 maxBufferedRows=2 + 3 组的回归测试。

switch (root) {
case "TINYINT": value = number.byteValueExact(); break;
case "SMALLINT": value = number.shortValueExact(); break;
case "INTEGER": value = number.intValueExact(); break;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[高] 合法数值谓词被拒,并报成"invalid SELECT syntax"

DoubleValue 现在变成 BigDecimal,整数列走 byteValueExact/shortValueExact/intValueExact/longValueExact。WHERE id > 1.5、WHERE id < 3000000000(INT 列)是合法 SQL,却抛 ArithmeticException,被 parse() 的 catch (Exception) 包装为 Unsupported Paimon SQL: invalid SELECT syntax(实测确认)。
旧实现走 PredicateBuilder.convertJavaObject,实测 1.5→1、3000000000→-1294967296,即静默截断。拒绝比错谓语安全,但①这是行为回归,②用户看到的是"语法错误",误导排障,③新测试把该行为固化为期望。
建议:优先精确改写比较(id > 1.5 → id >= 2;越界字面量 → 恒真/恒假);至少把 ArithmeticException 单独捕获,抛出 literal not representable as INT 类明确错误,并在文档与测试中断言该消息。

@Override
protected void execute(String sql, Consumer<Object> consumer, int batchSize, int queryTimeoutSeconds) throws Throwable {
JdbcQueryExecutor.execute(sql, jdbcContext::getConnection, consumer, this::isAlive,
batchSize, queryTimeoutSeconds, Integer.MIN_VALUE, resultSet -> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] MySQL 流式读的超时覆盖范围需在文档说明

该路径 setFetchSize(Integer.MIN_VALUE)(Connector/J 流式结果集),而 Statement.setQueryTimeout 的计时绑定在语句执行上;execute() 返回后的逐行迭代时间可能不受该超时约束。mysql_* 文档却写成"正值通过 JDBC Statement.setQueryTimeout 生效",读起来像整条命令有界。
建议:在 mysql 三语文档注明该限制(超时仅约束语句执行阶段,流式行读取可能超出),或对真实 MySQL 验证后写明实测结论。


Projection, aggregation and sorted queries allow non-key filters and full scans by default. Set connection `queryAllowFullScan=false` (or command boolean `allowFullScan=false`) to opt into the partition/leading-primary-key filter requirement. Command options take precedence; direct/wrapped `COUNT(1)` remains exempt, while the gate also applies to historical unsorted `SELECT *`. For `AND`, any qualifying condition is enough; for `OR`, every branch must constrain an eligible key. Same-key equality alternatives can be rewritten as `IN`. Positive `IS NULL` on a key qualifies; `NOT` and negated forms do not. Use `allowFullScan=true` only when a full scan is intended. A supported filter is pushed down, but SQL syntax alone does not guarantee file pruning.

The 1000-row inspection limit belongs to the engine, not the connector. It counts result rows across callbacks and fails on row 1001, discarding all partial results. `executeCommand` uses its existing `batchSize` and result callback to stream unordered projections; a consumer exception stops reading and closes resources. Aggregation and sorting must finish scanning before final rows are delivered; input rows are not capped at 1000. Ordered queries buffer result rows in memory up to `maxBufferedRows`; group-by aggregation keeps one state per group up to `maxGroups`. Direct command callers can receive more than 1000 result rows. Streaming callers must discard already received batches if a later callback reports an error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 文档对 ORDER BY 缓冲上限的描述对聚合查询不成立

三语文档(en_US:129、zh_CN:183、zh_TW:128)均称"带 ORDER BY 的查询会把结果行缓存到内存,上限为 maxBufferedRows",但 GROUP BY … ORDER BY 实际只受 maxGroups 约束(见 PaimonQueryPlan.java:344 的检查缺口)。修复 M1 后文档才成立;否则应改写为"聚合 + 排序受 maxGroups 约束"。
建议:与 M1 的代码修复一起落地,并在参数表 maxBufferedRows 行补一句"聚合查询按组缓存,受 maxGroups 约束"。

assertEquals(3, streamed.drain().size());
assertEquals(0, streamed.bufferedRows());
// Grouped results are capped by maxGroups and keep one state per group, not one row per input.
PaimonQueryPlan.Accumulator grouped = plan("SELECT region, COUNT(*) AS n FROM orders GROUP BY region")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 聚合缓冲上限的测试缺口

orderedQueriesBoundBufferedRowsWhileStreamingAndAggregatesDoNot 的聚合分支(:197-200)用的是无 ORDER BY 的 GROUP BY,因此永远不会走到 finish() 中缺失的检查;用例名还把"聚合不受 maxBufferedRows 限制"描述成预期行为,等于把 bug 写进了约束。
建议:修复 M1 时新增用例:maxBufferedRows=2,GROUP BY region ORDER BY region 覆盖 3 个分组,断言错误消息包含 maxBufferedRows。

"SELECT id, SUM(id) FROM orders",
"SELECT SUM(id) OVER () FROM orders",
"SELECT id + 1 FROM orders",
"SELECT * FROM orders WHERE id = 1.5",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 解析器测试把误导性报错固化为期望

新增的拒绝列表包含 WHERE id = 1.5、WHERE id = 2147483648,只断言 IllegalArgumentException 类型,因此"合法 SQL 被报成 invalid SELECT syntax"这个缺陷不会被测试发现;文档也只说支持"列与常量比较",从未说明超范围字面量会被拒绝。
建议:随 M2 一起调整——若选择精确改写则把这些用例移出拒绝列表并断言改写后的语义;若保留拒绝,则断言消息(如 "not representable as INT")并在三语文档写明规则。


protected void execute(String sql, Consumer<Object> consumer, int batchSize, int queryTimeoutSeconds) throws Throwable {
if (queryTimeoutSeconds == 0) {
execute(sql, consumer, batchSize);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 3 参/4 参 execute 重载可能互相递归直至栈溢出

基类 4 参在 timeout==0 时回调 execute(sql, consumer, batchSize)(虚拟分派)。若子类按"只重写 3 参、内部委托 4 参"的写法(execute(sql,c,b){ execute(sql,c,b,0); }),而自己没有重写 4 参,就会 3 参↔基类 4 参无限递归 → StackOverflowError。当前仓库内只有 MysqlConnector 重写且两个都重写了,所以尚未触发。
建议:在 4 参的 timeout==0 分支直接用 executeJdbcQuery(...)(仅在检测到 3 参被重写时才委派),或在方法注释/README 明确"3 参重写不得回调 4 参"的契约。

// A subclass that implements the timeout overload can deliberately delegate to super.
boolean timeoutOverridden = false;
boolean legacyOverridden = false;
for (Class<?> type = getClass(); type != CommonDbConnector.class; type = type.getSuperclass()) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 每次正超时调用都用反射遍历类层级

getDeclaredMethod 逐层扫描的结果对同一个类永远不变,却在每次调用时重算;并且 private 方法或 bridge 方法同名也会被记为"已重写",导致本应报 UnsupportedOperationException 的连接器静默走通用 JDBC 路径。
建议:用 ClassValue 缓存判定结果,并要求方法 modifier 非 private、非 bridge/synthetic。

private Object value;
private State(Item item) { this.item = item; }
void add(Object next) {
if (item.function == null) { value = next; return; }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 分组浮点列的展示值依赖扫描顺序

groupKey 已把 -0.0/+0.0 归一化到同一分组,但普通分组列的 State.add 直接 value = next(最后到达者胜出)。同一分组内同时出现 -0.0 与 0.0 时,展示哪个符号取决于扫描顺序,不影响相等性,但会造成跨端 diff 噪声。
建议:对浮点列在存储时同样归一化(如 value = groupKey(next) 或统一存 +0.0),并补一条断言。

throw unsupported("ORDER BY ordinal is out of range");
}
for (int i = 0; i < items.size(); i++) {
if (expression instanceof Column ? aliasMatches(items.get(i).label, ((Column) expression).getColumnName())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] ORDER BY 匹配忽略列表限定符

orderIndex 只用 getColumnName() 做别名匹配,因此 SELECT col FROM t ORDER BY other_table.col 会静默匹配已选 col;而 fieldIndex 对其它表限定符是显式拒绝的,行为不一致。
建议:当 Column 带 table qualifier 时,先校验其等于当前表名,否则抛 unsupported("ORDER BY references another table: ..."),与 fieldIndex 保持一致。

if (!sorts.isEmpty()) {
limits.check();
Comparator<Object[]> comparator = (a, b) -> {
try { limits.check(); }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 排序比较器每次比较都读时钟并检查中断

limits.check() 里含 clock.getAsLong()(超时开启时)与 Thread.interrupted 检查,被放在比较器内部,最多约 100k 行的排序会产生 O(n log n) 次系统调用级读取。
建议:按比较次数节流(例如每 1024 次比较检查一次),既保持取消响应性又显著降低开销。

<source>${java.version}</source>
<target>${java.version}</target>
<!-- Existing integration tests use Java 11 APIs; production stays Java 8. -->
<testSource>11</testSource>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 编译目标回退到 Java 8,但缺少 校验

删除重复的 compiler 插件后,生产代码从 Java 11 字节码回到 8;新增 testSource/testTarget=11 意味着构建需要 JDK 11+。当前主源码没有 Java 9+ API(已 grep List.of/isBlank/var/Stream.toList() 等),但 javac 未对 JDK 8 API 做校验,未来回归不会在编译期被发现。
建议:把 <source>/<target> 换成 <release>8</release>(测试用 testRelease 11),让编译期真正按 JDK 8 API 校验。

- [Apache Paimon 文檔](https://paimon.apache.org/)
- [Paimon GitHub 倉庫](https://github.com/apache/paimon)

## 自訂唯讀 SQL(`executeQuery`)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] "限制"章节仍称只支持写入,与新增 executeQuery 章节矛盾

第 89 行(en_US:90、zh_CN:110 同)写"此連接器目前僅支持寫入操作。不支持讀取和 CDC 操作",而紧随其后的 executeQuery 章节(:122 起)详细描述了只读 SQL 查询。
建议:把该条收窄为"不支持 CDC/流式读取(仅支持只读 ad-hoc 查询)",三语同步。


| 参数 | 默认值 | 含义 |
| --- | --- | --- |
| `queryTimeoutSeconds` | 0 | 非负整数秒;省略或 0 不设超时,正值在 catalog/规划/批次/行/排序/结果阶段检查协作式截止时间 |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 三语参数表在 queryTimeoutSeconds 行不一致

en_US(:135)与 zh_TW(:134)写明范围 0..2147483647,zh_CN 只描述协作式检查点而缺范围。
建议:zh_CN 补齐范围与"JDBC 上界一致"的说明,使三语参数表一致。

@ValueSource(strings = {"UTC", "Asia/Shanghai", "America/Los_Angeles"})
void realPaimonSelectStarPreservesTemporalJavaTypes(String zone) throws Exception {
java.util.TimeZone previous = java.util.TimeZone.getDefault();
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(zone));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 测试可在失败时污染 JVM 时区

realPaimonSelectStarPreservesTemporalJavaTypes 在 try 之前调用 TimeZone.setDefault(...) 与 new PaimonService(...);若构造或 config() 抛异常,finally 不执行,时区会残留给同 JVM 的后续测试。兄弟用例 executesCustomerDateAggregation...(:164)已把这两步放进 try。
建议:把 setDefault 之后的 new PaimonService(...) 移入 try,或用 try/finally 包住整个方法体。

SQLTimeoutException failure = assertThrows(SQLTimeoutException.class, accumulator::finish);
assertTrue(failure.getMessage().contains("queryTimeoutSeconds=1"));
// Constructor, finish, pre-sort and first comparator: expiry occurs during comparison.
assertEquals(1_600_000_000L, ticks.get());

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 超时测试依赖精确的时钟读取次数

assertEquals(1_600_000_000L, ticks.get()) 只有在恰好 4 次 check() 时才成立;一旦按建议给比较器节流(或新增检查点),行为仍正确但测试会误报。
建议:只断言抛出 SQLTimeoutException 且消息含 queryTimeoutSeconds=1,并断言比较器确实执行过(例如计数器 > 0),不绑定 tick 具体数值。

- Bound materialized groups for GROUP BY ... ORDER BY by maxBufferedRows, and
  cover the gap with a regression test that previously returned rows silently.
- Report fractional or out-of-range integer literals as "not representable as
  <type>" instead of wrapping them in "invalid SELECT syntax"; drop the now
  unreachable Double/DECIMAL literal branches and assert the message in tests.
- Normalize signed zeros for grouped float columns so the displayed sign does
  not depend on scan order; reject ORDER BY references to another table.
- Sample the sort deadline (first comparison, then every 1024) instead of
  reading the clock on every comparison, and stop pinning the tick count.
- Cache execute-override detection per class, ignore private/bridge methods,
  and fail with a clear error when the legacy overload delegates back instead
  of overflowing the stack.
- Keep the deprecated SqlExecuteCommandFunction path on the streaming path so
  queryTimeoutSeconds is no longer silently dropped.
- Compile production code with <release>8</release> (testRelease 11) so Java 9+
  APIs fail at build time.
- Document the MySQL streaming timeout scope, ordered aggregate buffering and
  integer literal rules in all three languages; fix the stale "write only"
  limitation and the incomplete zh_CN timeout range; move PaimonService
  construction inside the integration test's try block.
@jarad0628

Copy link
Copy Markdown
Collaborator Author

⚠️ Design Decision Required

This PR has undergone multiple review cycles with recurring issues, suggesting a need for upfront design clarification rather than continued incremental fixes.

核查完成(head efcc6a52,读了 PaimonQueryPlan/PaimonQueryLimits/PaimonService.executeQueryAdmitted/CommonDbConnector/DefaultSqlExecutor/JdbcQueryExecutor/SqlCommandParams,并看了 develop...HEAD 的 3.6k 行 diff)。


设计决策请求:executeQuery 是"薄透传"还是"有界 SQL 引擎"?

结论先行

9 轮 review、115 条 finding(9 条阻断),没有一条是孤立的实现 bug。它们全部来自一个未声明的设计选择:本 PR 让 executeQuery 同时扮演两个互斥的角色,并在运行时按 SQL 的形状逐查询决定这次扮演哪个。

只要这个身份没有明确,下一轮 review 必然还会发现新的"静默回归" —— 因为每次修复都在同一层模糊地带上加了一个补丁,而补丁本身又制造了新的边界。

115 条 finding 的四个根源

根源 典型 finding(轮次) 本质
① 公共 SPI 形状变更:SqlExecutor 新增 6 参 default 方法、CommonDbConnector 3/4 参互调、DefaultSqlExecutor 5/6 参、废弃 executeCommand 改道 R1/R2/R3/R5/R6 的"丢弃超时"、R3/R8 的"3 参递归"、R7/R9 的"只覆写 5 参被子类被绕过"、R9 的"空结果集不再回调" 想在不破坏子类覆写的前提下加超时,最后演化成 ClassValue 反射探测 + ThreadLocal 递归守卫 + UnsupportedOperationException 契约(CommonDbConnector.java:1152-1204)—— 这套机制本身就是"接口形状不对"的症状
② 兼容性用 SQL 形状启发式表达:PaimonQueryPlan.legacyRows() = star && sorts.isEmpty() && !aggregate(:250) R4 高"SELECT * 返回 Java 类型变了"、R7 高"±0.0"、R7 中"COUNT(*) vs COUNT(1) 语义分裂"、R9 中"SELECT * 含复杂列从可用变失败" 一个布尔同时决定扫描限额(PaimonService.java:3175 的 forLegacyQuery())、全表门禁豁免、返回形状与 Java 类型三件正交的事。任何一件要改,就会从另外两件的表象上冒出来
③ 限额/超时语义没有单一权威:默认值、0 的含义、上限优先级 R1/R2/R3 高"300s 默认作用于所有既有查询"、"1000 行默认让既有结果直接失败"、R8 高"聚合+ORDER BY 绕过 maxBufferedRows"、R3 低"batchSize 接受 0/负数" 同一参数在 sql-core 与 Paimon 两侧语义相反或缺失;SqlCommandParams.getInteger 对 <=0 静默回落,getQueryTimeoutSeconds 却显式报错(SqlCommandParams.java:17 vs :28)—— 同一个类里两套口径
④ 一个 PR 装了两个产品级变更 全部 sql-core 超时管道(影响所有 SQL 连接器)+ paimon 自建 SQL 引擎(560 行 plan + 624 行 parser + 102 行 limits + 三语文档 ≈ 3.6k 行)

核心两难

executeQuery 对外承诺的是哪一种东西?

  • 若是薄透传("你把 SQL 给我,我按基线那样把行给你"),那它的契约必须逐位不变:SELECT * 的 Java 类型、空结果集回调次数、子类覆写点、未加限制的扫描——一条都不能动。
  • 若是有界 SQL 引擎("我只支持这个子集,有上限、有超时、类型有映射表"),那它必须有自己的显式契约和退出通道,旧的透传语义要能被关掉。

当前代码选了第三条路:不声明,用 legacyRows() 这个形状谓词在运行时猜。这解释了为什么 R4 已经因为 SELECT * 类型变化被阻断,R9 又因为 SELECT * 复杂列再被阻断一次——同一处模糊地带,两次不同的侧面。

三个可选方向

方案 A:拆 PR + 显式开关(params.queryEngine=legacy|semantic,默认 legacy)

做什么

  • PR-1(sql-core,约 150 行):不做 SPI 形状变更。JdbcQueryExecutor 保留为新工具类,但只被新增的 opt-in 方法使用;execute(sql, consumer, batchSize) 与 4 参重载的现有分派语义原样不动,删掉 ClassValue 探测与 ThreadLocal 守卫;废弃 executeCommand 完全不动(保持"恰好一次回调、空结果为 []")。
  • PR-2(paimon):executeQueryAdmitted 入口按 queryEngine 分支。legacy = 基线 convertInternalRowToMap 整行路径 + 仅加 maxScanRows/超时/流式;semantic 才走 PaimonQueryPlan。legacyRows() 启发式删除。

优点:既有一切行为 byte-identical(R4/R7/R9 三类兼容性 finding 直接消失);反射+ThreadLocal 那套机制不需要存在;两个 PR 各自可独立评审合入。
代价:两条路径要同时维护(行转换双实现);新能力默认关闭,需要 spec 增加一个字段并由使用方打开。
代码影响:PaimonService.executeQueryAdmitted 增加一个分支;CommonDbConnector 减少约 50 行(删探测机制);spec.json 三语各加一个枚举字段。

方案 B:单个 PR,但把契约写成单点分类(QueryKind 枚举)

做什么

  • PaimonSqlQueryParser.parse 结束时产出显式 QueryKind(LEGACY_ROW / BOUNDED_ENGINE / COUNT / STAR_LEGACY)并写进 ParsedQuery;限额、门禁豁免、返回形状、返回 Java 类型只读这一个枚举,不再散落。
  • SELECT * 含复杂列 → 回退 convertInternalRowToMap,不再报错(消除唯一的硬兼容破坏)。
  • sql-core 只保留 5 参作为唯一扩展点,删掉 6 参 default 方法与探测机制;超时走 SqlCommandParams 已解析好的值,在构造期决定。
  • 新增一节契约文档:支持语法、列类型→Java 类型映射表、回调次数、多结果集、限额优先级、超时语义(含"不可中断 I/O")。
  • 统一 SqlCommandParams 对 0/负数的口径(batchSize/maxResultRows/queryTimeoutSeconds 一致)。

优点:无新开关,用户可见行为一次说清;反射机制消失;三语文档有唯一权威来源。
代价:仍是 ~3.6k 行单一 diff,评审疲劳不会消失;必须一次性接受"新语义替代旧语义"(COUNT(1) 的返回类型、SELECT * 新引擎路径的类型映射);没有回退通道,线上遇到回归只能改代码。

方案 C:本 PR 只交超时+限额,聚合能力单独走后续

做什么:executeQuery 保持 convertInternalRowToMap;只加 maxScanRows / queryTimeoutSeconds / batch 流式 / allowFullScan 门禁;GROUP BY/ORDER BY/聚合表达式一律"明确不支持";聚合下推(Paimon compute 或客户端聚合)另开设计与 PR。

优点:9 轮 finding 里约 80% 直接消失;PR 的原始目的(JDBC 超时)立刻可合;契约面最小。
代价:需求方要的"聚合自定义 SQL"落不了地;已写的 1400 行 plan/parser 变 dead code 或另起 PR;需要在需求侧确认时间线。

我的建议

A 为主,B 作为 PR-2 的目标形态。 理由:本 PR 的 9 轮里,真正卡住合入的从来不是算法正确性,而是"改动了未声明的既有行为"。方案 A 把这类风险一次性归零(默认路径不变),同时让 PR-1 在 1 轮内可结束;PR-2 再用 B 的单点分类把契约固化,避免第五类根源在下一轮重现。

C 是风险最低但可能不满足需求的兜底;若需求方接受"聚合先不做",它的投入产出比其实最高。

需要作者/需求方确认的 3 个事实

  1. 聚合自定义 SQL 有真实用户在等吗? 若时间线可接受"opt-in 且默认关闭",选 A;若能延后,选 C。
  2. 现有 SELECT * 用户中,含 ARRAY/MAP/ROW 列的表在用吗? 这决定"从返回整行变为报错"能否作为可接受的破坏性变更(否则必须修成回退)。
  3. queryTimeoutSeconds 是产品级契约还是 Paimon 内部参数? 若影响所有 SQL 连接器,它应单独走一次 spec 评审,不应和 Paimon 引擎绑在同一个 PR 里。

在方向确定前,我不建议再做增量修复 —— 上一轮 18 项全修完,本轮仍新增 7 项,其中 3 项是中级的兼容性回归,趋势没有收敛。

Next Steps: Please review the options above and clarify your design intent. Once the approach is agreed upon, the implementation can proceed more smoothly.


This comment was automatically generated after detecting a back-and-forth pattern.

@jarad0628

Copy link
Copy Markdown
Collaborator Author

AI PR Review — ❌ 不可通行

  • PR: #954
  • Head: efcc6a520dd601d4409a76adaa4b4aaf15bf2463
  • Reviewers: codexcli, claudecli, reasonix

问题清单

中

  • connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:73 【上轮漏报】SELECT * 遇复杂列由返回整行变为整条命令失败
  • connectors-common/sql-core/src/main/java/io/tapdata/common/DefaultSqlExecutor.java:52 【上轮漏报】只覆写 5 参 execute 的 DefaultSqlExecutor 子类会被命令路径绕过
  • connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:42 废弃 executeCommand 改走流式路径,结果回调契约变了(空结果集不再回调)

低

  • connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonQueryPlanTest.java:263 超时用例的新断言测不出"排序中检查"
  • connectors/mysql-connector/src/main/resources/docs/mysql_en_US.md:87 MySQL 文档建议的超时兜底参数不正确
  • connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_TW.md:145 zh_TW 把整型字面量规则放错了段落
  • connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1163 递归守卫会误伤合法的嵌套查询

最终总结

我会先核对仓库实际代码,再逐条裁定。
核查完毕,我逐条用仓库实际代码(head efcc6a52)验证了两位 reviewer 的意见,并运行了两组测试。

验证方式

  • mvn -o test -Dtest=PaimonQueryPlanTest,PaimonSqlQueryParserTest → 25 用例全部通过(18 + 7)。
  • mvn -o test -Dtest=SqlCommandTimeoutTest(sql-core)→ 13 用例全部通过。
  • 逐行核对 PaimonQueryPlan/PaimonSqlQueryParser/CommonDbConnector/SqlExecuteCommandFunction/DefaultSqlExecutor/JdbcQueryExecutor 与三语文档,并与基线 c132b49f(merge-base)对比行为。

上轮 18 项复核结论

全部 18 项已修复或脆弱性已消除,无一仍存在或部分残留:

# 状态 证据
1 已修复 finish() 物化前检查 !sorts.isEmpty() && states.size() > maxBufferedRows(:348-352);新增 orderedAggregatesAlsoBoundMaterializedGroups(2 行上限 + 3 组,实测通过)
2 已修复 narrow() 抛 literal ... is not representable as ...(:526-538),parse() 的 catch (IllegalArgumentException) throw e;(:96-97)不再包装成语法错误;测试断言消息且 assertFalse(contains("invalid SELECT syntax"))
3 已修复(引出 N1) SqlExecuteCommandFunction.java:42 转入共享路径,超时生效并有测试
4 已修复(文档建议有误 → L2) mysql 三语文档补充流式读取超时范围
5 已修复 paimon 三语 maxBufferedRows 行与正文均写明聚合+排序受两个上限约束
6 已修复 见 #1
7 已修复 PaimonSqlQueryParserTest:117-129
8 已修复 CommonDbConnector:1161-1173 ThreadLocal 守卫 + delegatingLegacyOverrideFailsClearlyInsteadOfOverflowingTheStack
9 已修复 ClassValue OVERRIDDEN + 排除 private/bridge/synthetic(:1181-1204)
10 已修复 删除 -(Double) 与 convertLiteral 的 DECIMAL 分支(我核实两条确为不可达:typedLiteral 对 DecimalType 提前返回)
11 已修复 normalizeZero 同时用于 groupKey 与 State.add,保留 Float 类型,测试断言 +0.0
12 已修复 orderIndex 校验表限定符(:143-151)
13 已修复 每 1024 次比较采样,& 1023 对负值仍正确
14 已修复 <release>${java.version}</release>(8) + <testRelease>11</testRelease>,Compiler Plugin 3.7.0 支持
15 已修复 三语"限制"改为"不支持 CDC/流式读取,只读查询用 executeQuery"
16 已修复 zh_CN 补 0..2147483647(与 JDBC 上界一致)
17 已修复 构造移入 try,finally 判空
18 已修复(新断言无效 → L1) 不再绑定 tick 精确值,但 assertTrue(ticks.get() > 0) 恒真

本轮新发现(7 项)

  • [中] SELECT * 遇复杂列由"返回整行"变为整条命令失败:星号投影逐列调用 column(),ARRAY/MAP/MULTISET/ROW 直接抛错(:73 → :177)。基线 executeQuery 走 convertInternalRowToMap,同一行里的标量列原本是可用的(复杂列本身经 default: row.getString(pos) 得到的是无意义值,并非正确结构——两位 reviewer 的"基线可以处理这些列"表述略过头,但"查询从返回变为失败"成立)。文档已写限制,但这是兼容性行为变更,且无任何测试覆盖该拒绝路径。
  • [中] 命令路径绕过只覆写 5 参 execute 的 DefaultSqlExecutor 子类:6 参直接执行 JDBC(:52-60),命令路径调 6 参;仓库内无此类子类,外部子类的自定义行转换会静默失效。
  • [中] 废弃 executeCommand 改走流式路径,回调契约变了:JdbcQueryExecutor:58 的 if (!batch.isEmpty()) 意味着空结果集一次回调都不发(旧版必回调一次空列表);结果还按 batchSize 拆分、多结果集全部推送、废弃 2 参 execute 覆写被绕过。新测试 deprecatedCommandAlsoPassesTimeoutToJdbc 只走了 DML 路径,空 SELECT/多批次均未覆盖。
  • [低] 超时测试仍未验证"排序中检查":PaimonQueryLimits 构造即 clock.getAsLong(),ticks > 0 恒真;我按 +400ms 推演,即便删掉比较器里的检查,排序后的 limits.check() 也会在第 4 次读取(1.2s)抛 SQLTimeoutException,用例照样通过。
  • [低] MySQL 文档给出的限流参数有误:socketTimeout 是 Connector/J 客户端连接属性而非服务端;net_read_timeout 约束服务端读客户端;限制服务端推送流式结果的是 net_write_timeout(Connector/J 的 netTimeoutForStreamingResults 即调它)。三语皆需更正。
  • [低] zh_TW 把"整型字面量只接受整数值"写进了 ORDER BY 段,en_US/zh_CN 都放在 WHERE 段。
  • [低] ThreadLocal 递归守卫会误伤合法嵌套查询:只覆写 3 参的子类在自己的方法里用 4 参(超时 0)执行另一条查询会被判为递归并抛 IllegalStateException,报错信息误导排查。

被驳回的发现

无。两位 reviewer 的具体意见均复核成立(仅对"基线能正确处理复杂列"这一理由做了修正,结论不变)。GitHub 上未见晚于基线 head 的新人工评论;此前评论提到的复杂列兼容性问题由上面第 1 条覆盖。

下一步

  1. 对含复杂列的表,SELECT * 改为"标量列照旧、复杂列报错或省略"的可选策略(或至少补一条拒绝路径测试并在文档写明迁移),消除兼容性回归。
  2. DefaultSqlExecutor / 废弃入口采用与 CommonDbConnector 相同的覆写探测:超时 0 走旧路径,正值明确报错;并用一条空 SELECT 用例把"恰好一次回调"的契约固定下来。
  3. 让超时用例真正验证"排序中取消"(在比较器采样那次到期,或断言 SortCancelled 路径走过)。
  4. 修正 MySQL 三语文档的 socketTimeout/net_read_timeout 表述,把 zh_TW 的整型字面量规则移到 WHERE 段,并在递归守卫处写明契约。

FINAL_VERDICT: BLOCK

Design decision required: Review history shows an A→B→A finding cycle. Clarify the design choice before continuing.

codexcli review

Findings

  • [中] 【上轮漏报】SELECT * 遇到复杂列会从返回整行变为报错 — PaimonQueryPlan.java。星号投影逐列调用 column();该方法在 177–178 行拒绝 ARRAY/MAP/MULTISET/ROW。基线版本的 executeQuery 则把整行交给 convertInternalRowToMap。这会让原本可用的 SELECT * 在含复杂列的表上失败;当前文档也记录了该限制,但没有消除兼容性回归。
  • [中] 【上轮漏报】只覆写 5 参方法的 DefaultSqlExecutor 子类会被绕过 — DefaultSqlExecutor.java。命令路径调用 6 参方法;其实现直接执行 JDBC 路径(50–60 行),不会分派到子类覆写的 5 参方法。外部子类的自定义读取或转换逻辑因此失效;仓库内未发现该类的子类。

上轮 18 项复核

以下均为已修复;括号内为当前代码证据。

# 原 finding 状态与证据
1 聚合排序绕过 maxBufferedRows 已修复:finish() 检查分组数,348–352 行。
2 整数谓词字面量报成语法错误 已修复:narrow() 给出不可表示错误,526–538 行。
3 deprecated 命令入口丢弃超时 已修复:转入带 aliveSupplier 的共享路径,38–43 行。
4 MySQL 流式读取超时范围未说明 已修复:MySQL 三语文档注明超时覆盖范围。
5 聚合排序的文档上限不成立 已修复:三语文档说明同时受两个上限约束。
6 缺聚合排序缓冲上限测试 已修复:测试以 2 行上限、3 组断言失败。
7 解析器测试固化误导性错误 已修复:测试断言不可表示错误消息且排除语法错误消息。
8 3/4 参 execute 可能递归 已修复:ThreadLocal 检测并明确失败,1161–1173 行;有回归测试。
9 反射重复扫描且误判代理方法 已修复:ClassValue 缓存并排除 private/bridge/synthetic,1181–1204 行。
10 字面量转换存在不可达分支 已修复:移除死分支。
11 分组浮点展示值受扫描顺序影响 已修复:分组键和展示值都归一化正负零,315–316、502–505 行。
12 ORDER BY 忽略表限定符 已修复:校验限定表名,143–150 行。
13 排序比较器每次比较都检查时钟 已修复:每 1024 次比较采样,364–370 行。
14 Java 8 编译缺少 API 面校验 已修复:配置 <release>8</release> 和 <testRelease>11</testRelease>;Compiler Plugin 3.7.0 支持 testRelease。官方插件文档
15 Paimon 限制章节与查询功能矛盾 已修复:三语文档改为不支持 CDC/流式读取,注明可用只读查询。
16 中文超时参数表缺少范围 已修复:范围为 0..2147483647,并注明 JDBC 上界。
17 集成测试异常时可能污染 JVM 时区 已修复:服务构造移入 try,finally 恢复时区。
18 超时测试绑定时钟读取次数 已修复:改为只断言实际发生时钟检查。

GitHub 上未发现晚于基线 head 1db667577655032f7e33253d3cf8e018d4cc3a91 的新人工评论;此前评论中提到的复杂列兼容性问题仍成立。未运行构建或测试;git diff --check 通过。

VERDICT: BLOCK

claudecli review

PR #954 增量审查(1db66757 → efcc6a52)

我读了 head checkout 的代码,没有构建,也没有运行测试。上轮 18 项全部修复。本轮的改动新引入 1 个中级问题;另外我确认了 Codex 报的 2 个上轮漏报,它们也是中级。所以仍然阻断合并。

上轮 18 项复核

# 问题 状态 代码证据
1 聚合 + ORDER BY 绕过 maxBufferedRows 已修复 PaimonQueryPlan.finish() 在物化前检查分组数(:348-352),并有 orderedAggregatesAlsoBoundMaterializedGroups 测试
2 合法整数谓词被报成语法错误 已修复 作者没有改写比较,而是给出明确报错,即上轮建议中的保底方案。narrow() 抛出 "not representable as …"(:526-538)。这个异常是 IllegalArgumentException,parse() 的 catch (IllegalArgumentException) 会原样抛出,不会再包装成语法错误
3 废弃的 executeCommand 丢弃超时 已修复,但引入了新问题 见新问题 N1
4 MySQL 流式读超时的覆盖范围 已修复 三语文档已补充说明,但给出的配置建议有误,见 L2
5 文档的缓冲上限描述 已修复 三语文档都已说明聚合排序同时受两个上限约束
6 聚合缓冲上限缺测试 已修复 新用例为 maxBufferedRows=2、3 组,并断言错误消息
7 解析器测试固化了误导性报错 已修复 测试改为断言消息内容,并排除 "invalid SELECT syntax"
8 3 参 / 4 参 execute 互相递归 已修复 用 ThreadLocal 守卫并明确报错(CommonDbConnector.java:1163-1173),有回归测试
9 每次调用都用反射扫描,且误判 private/bridge 方法 已修复 改用 ClassValue 缓存,并排除 private、bridge、synthetic 方法
10 字面量改动留下死分支 已修复 两处死分支都已删除
11 分组浮点列展示值依赖扫描顺序 已修复 新增 normalizeZero,分组键和 State.add 都经过它,测试断言 +0.0
12 ORDER BY 忽略表限定符 已修复 限定符校验与 fieldIndex 一致(:145-151)
13 排序比较器每次比较都读时钟 已修复 改为每 1024 次比较检查一次;计数器 int 溢出后 & 1023 仍然正确
14 缺少 <release> 校验 已修复 用 <release>${java.version}</release>(值为 8)加 <testRelease>11</testRelease>;主源码没有 sun.* 或 jdk.internal 依赖,--release 8 不会误报
15 "仅支持写入"的说法自相矛盾 已修复 三语文档都已改为"不支持 CDC 和流式读取"
16 zh_CN 文档缺超时范围 已修复 已补上 0..2147483647
17 集成测试可能污染 JVM 时区 已修复 构造移入 try,finally 对 null 做了判断
18 测试绑定时钟读取次数 已修复,但新断言测不出问题 见 L1

新问题

[中] N1:废弃的 executeCommand 改走流式路径,返回结果的约定变了(SqlExecuteCommandFunction.java:39-43,JdbcQueryExecutor.java:51-60)

  • 原来的行为: 调用 sqlExecutor.execute(sql, supplier)。每条命令正好回调一次:SELECT 回调一次完整的 List<DataMap>,结果为空时是空列表;出错时回调一个 error 结果。
  • 现在的行为: 走 6 参流式路径,有三处变化:
    1. 结果按 batchSize(默认 1000)拆成多次回调。
    2. SELECT 结果为空时一次都不回调(if (!batch.isEmpty()))。旧版调用方如果等"恰好一个结果",会挂起,或者当成没有返回。
    3. 多结果集会全部推出来;而自定义 SqlExecutor 里改写过的废弃 2 参 execute 会被绕过。
  • 影响范围: 仓库内的调用方(Hive、Hive1、Hudi、Databend、Snowflake)都用带 aliveSupplier 的重载,不受影响;受影响的只有外部或遗留调用方。新测试 deprecatedCommandAlsoPassesTimeoutToJdbc 只覆盖了更新计数的路径,空 SELECT 和多批次都没测到。
  • 建议: 废弃入口保留原来的单结果路径。超时为 0 时行为完全不变;为正值时,要么在旧路径里调用 setQueryTimeout,要么明确报错。

[中]【上轮漏报】SELECT * 遇到复杂类型列,从返回整行变成报错(PaimonQueryPlan.java:70-73、:177-178)

  • 星号投影会对每一列调用 column(),而 column() 拒绝 ARRAY、MAP、MULTISET、ROW。
  • 基线版本的 PaimonService.executeQuery(base 第 3172 行)用 convertInternalRowToMap 转换整行,可以处理这些列。
  • 所以对含复杂列的表,原本能用的 SELECT * 现在直接失败。文档写了这个限制,但这仍是兼容性回归。我同意 Codex 的结论。

[中]【上轮漏报】只改写 5 参 execute 的 DefaultSqlExecutor 子类会被绕过(DefaultSqlExecutor.java:45-60)

  • 5 参方法委派给 6 参,而 6 参直接走 JDBC,不会回调 5 参。命令路径调用的是 6 参。
  • 结果是:子类在 5 参里写的自定义转换不再生效。仓库内没有这样的子类,但现在废弃入口也走这条路径(N1),影响面变大了。
  • 建议: 用与 CommonDbConnector 相同的覆盖检测:超时为 0 时回调 5 参,为正值时明确报错。

[低] L1:超时测试的新断言测不出问题(PaimonQueryPlanTest.java:251-263)

  • 构造函数读一次时钟,所以 ticks.get() > 0 永远成立。
  • 按每次 +400ms 推算:如果删掉比较器里的检查,排序结束后的 limits.check() 恰好在第 4 次读时钟时(1.2s)到期,同样抛出 SQLTimeoutException。因此测试名说的"排序中检查截止时间"其实没被验证。
  • 建议: 在比较器触发检查的那次让时钟到期,并断言超时发生在排序之前(比如断言排序后的检查没有执行到),或者断言 SortCancelled 这条路径确实走过。

[低] L2:MySQL 文档建议的超时参数不对(mysql_*.md:87)

  • 它说可以配合"服务端 socketTimeout/net_read_timeout"来限制逐行读取。但 socketTimeout 是 Connector/J 客户端属性,不是服务端设置。
  • net_read_timeout 控制服务端从客户端读数据,与流式结果的推送无关。真正相关的是 net_write_timeout;Connector/J 的 netTimeoutForStreamingResults 调的就是它。
  • batchSize 也不限制时间。三语都需要更正。

[低] L3:zh_TW 文档的整型字面量说明放错了段落(paimon_zh_TW.md)

  • 这条规则被写进了 ORDER BY 那段("排序需將結果行快取…;整型欄位只接受…")。en_US 和 zh_CN 都放在 WHERE 那段。
  • 应移到下面"WHERE 支援…"那段。

[低] L4:递归守卫会误伤合法的嵌套查询(CommonDbConnector.java:1163-1166)

  • 守卫以线程为单位。如果某个只改写 3 参的子类,在自己的方法里用 4 参(超时 0)再执行一个不同的查询,比如预查询,也会被当成递归报 IllegalStateException。
  • 这种情况很少见,但报错信息会误导排查。建议在注释里写明这个契约。

已检查、没有发现问题

  • ClassValue 的计算会沿类层级逐层往上,MySQL 的子类能继承到"已覆盖"的标记。
  • finally 里的 remove() 不会把外层调用设的标记残留下来。
  • narrow() 对 1.0 这类没有小数部分的值能正常转换;BETWEEN 也走同一路径,测试覆盖了。
  • normalizeZero 现在把 Float 保留为 Float 键,不再转成 Double;同一列类型内相等性不受影响,NaN 仍然归为一组。
  • 父 POM 的 source/target 会合并进来,但设置了 release 时 3.7.0 优先使用 --release。

VERDICT: BLOCK

reasonix review

我会先核对仓库实际代码,再逐条裁定。
核查完毕,我逐条用仓库实际代码(head efcc6a52)验证了两位 reviewer 的意见,并运行了两组测试。

验证方式

  • mvn -o test -Dtest=PaimonQueryPlanTest,PaimonSqlQueryParserTest → 25 用例全部通过(18 + 7)。
  • mvn -o test -Dtest=SqlCommandTimeoutTest(sql-core)→ 13 用例全部通过。
  • 逐行核对 PaimonQueryPlan/PaimonSqlQueryParser/CommonDbConnector/SqlExecuteCommandFunction/DefaultSqlExecutor/JdbcQueryExecutor 与三语文档,并与基线 c132b49f(merge-base)对比行为。

上轮 18 项复核结论

全部 18 项已修复或脆弱性已消除,无一仍存在或部分残留:

# 状态 证据
1 已修复 finish() 物化前检查 !sorts.isEmpty() && states.size() > maxBufferedRows(:348-352);新增 orderedAggregatesAlsoBoundMaterializedGroups(2 行上限 + 3 组,实测通过)
2 已修复 narrow() 抛 literal ... is not representable as ...(:526-538),parse() 的 catch (IllegalArgumentException) throw e;(:96-97)不再包装成语法错误;测试断言消息且 assertFalse(contains("invalid SELECT syntax"))
3 已修复(引出 N1) SqlExecuteCommandFunction.java:42 转入共享路径,超时生效并有测试
4 已修复(文档建议有误 → L2) mysql 三语文档补充流式读取超时范围
5 已修复 paimon 三语 maxBufferedRows 行与正文均写明聚合+排序受两个上限约束
6 已修复 见 #1
7 已修复 PaimonSqlQueryParserTest:117-129
8 已修复 CommonDbConnector:1161-1173 ThreadLocal 守卫 + delegatingLegacyOverrideFailsClearlyInsteadOfOverflowingTheStack
9 已修复 ClassValue OVERRIDDEN + 排除 private/bridge/synthetic(:1181-1204)
10 已修复 删除 -(Double) 与 convertLiteral 的 DECIMAL 分支(我核实两条确为不可达:typedLiteral 对 DecimalType 提前返回)
11 已修复 normalizeZero 同时用于 groupKey 与 State.add,保留 Float 类型,测试断言 +0.0
12 已修复 orderIndex 校验表限定符(:143-151)
13 已修复 每 1024 次比较采样,& 1023 对负值仍正确
14 已修复 <release>${java.version}</release>(8) + <testRelease>11</testRelease>,Compiler Plugin 3.7.0 支持
15 已修复 三语"限制"改为"不支持 CDC/流式读取,只读查询用 executeQuery"
16 已修复 zh_CN 补 0..2147483647(与 JDBC 上界一致)
17 已修复 构造移入 try,finally 判空
18 已修复(新断言无效 → L1) 不再绑定 tick 精确值,但 assertTrue(ticks.get() > 0) 恒真

本轮新发现(7 项)

  • [中] SELECT * 遇复杂列由"返回整行"变为整条命令失败:星号投影逐列调用 column(),ARRAY/MAP/MULTISET/ROW 直接抛错(:73 → :177)。基线 executeQuery 走 convertInternalRowToMap,同一行里的标量列原本是可用的(复杂列本身经 default: row.getString(pos) 得到的是无意义值,并非正确结构——两位 reviewer 的"基线可以处理这些列"表述略过头,但"查询从返回变为失败"成立)。文档已写限制,但这是兼容性行为变更,且无任何测试覆盖该拒绝路径。
  • [中] 命令路径绕过只覆写 5 参 execute 的 DefaultSqlExecutor 子类:6 参直接执行 JDBC(:52-60),命令路径调 6 参;仓库内无此类子类,外部子类的自定义行转换会静默失效。
  • [中] 废弃 executeCommand 改走流式路径,回调契约变了:JdbcQueryExecutor:58 的 if (!batch.isEmpty()) 意味着空结果集一次回调都不发(旧版必回调一次空列表);结果还按 batchSize 拆分、多结果集全部推送、废弃 2 参 execute 覆写被绕过。新测试 deprecatedCommandAlsoPassesTimeoutToJdbc 只走了 DML 路径,空 SELECT/多批次均未覆盖。
  • [低] 超时测试仍未验证"排序中检查":PaimonQueryLimits 构造即 clock.getAsLong(),ticks > 0 恒真;我按 +400ms 推演,即便删掉比较器里的检查,排序后的 limits.check() 也会在第 4 次读取(1.2s)抛 SQLTimeoutException,用例照样通过。
  • [低] MySQL 文档给出的限流参数有误:socketTimeout 是 Connector/J 客户端连接属性而非服务端;net_read_timeout 约束服务端读客户端;限制服务端推送流式结果的是 net_write_timeout(Connector/J 的 netTimeoutForStreamingResults 即调它)。三语皆需更正。
  • [低] zh_TW 把"整型字面量只接受整数值"写进了 ORDER BY 段,en_US/zh_CN 都放在 WHERE 段。
  • [低] ThreadLocal 递归守卫会误伤合法嵌套查询:只覆写 3 参的子类在自己的方法里用 4 参(超时 0)执行另一条查询会被判为递归并抛 IllegalStateException,报错信息误导排查。

被驳回的发现

无。两位 reviewer 的具体意见均复核成立(仅对"基线能正确处理复杂列"这一理由做了修正,结论不变)。GitHub 上未见晚于基线 head 的新人工评论;此前评论提到的复杂列兼容性问题由上面第 1 条覆盖。

下一步

  1. 对含复杂列的表,SELECT * 改为"标量列照旧、复杂列报错或省略"的可选策略(或至少补一条拒绝路径测试并在文档写明迁移),消除兼容性回归。
  2. DefaultSqlExecutor / 废弃入口采用与 CommonDbConnector 相同的覆写探测:超时 0 走旧路径,正值明确报错;并用一条空 SELECT 用例把"恰好一次回调"的契约固定下来。
  3. 让超时用例真正验证"排序中取消"(在比较器采样那次到期,或断言 SortCancelled 路径走过)。
  4. 修正 MySQL 三语文档的 socketTimeout/net_read_timeout 表述,把 zh_TW 的整型字面量规则移到 WHERE 段,并在递归守卫处写明契约。

FINAL_VERDICT: BLOCK

===FINDINGS===
[中] connectors/paimon-plus-connector/src/main/java/io/tapdata/connector/paimon/service/PaimonQueryPlan.java:73
【上轮漏报】SELECT * 遇复杂列由返回整行变为整条命令失败
星号投影对每列调用 column(),而 column() 在 177-178 行拒绝 ARRAY/MAP/MULTISET/ROW,因此含任一复杂列的表执行 SELECT * 会整条失败。基线 executeQuery 走 convertInternalRowToMap:复杂列本身是用 default: row.getString(pos) 转出的无意义值,但同一行里的标量列是可用的;本次改动把"部分可用"变成"完全不可用",属于兼容性行为变更。三语文档虽已写明该限制,但没有任何测试覆盖这条拒绝路径,也没有给出参数化的兼容通道。建议:星号投影只物化标量列并对复杂列给出按列可选的错误/省略策略(或提供 params 开关回到旧行为),同时补一条 "SELECT * 含 ARRAY 列" 的解析测试,并在三语文档写明迁移步骤与错误信息。

[中] connectors-common/sql-core/src/main/java/io/tapdata/common/DefaultSqlExecutor.java:52
【上轮漏报】只覆写 5 参 execute 的 DefaultSqlExecutor 子类会被命令路径绕过
6 参方法直接调用 JdbcQueryExecutor.execute(52-60 行),不会分派到 5 参方法;5 参(44-47 行)只是委派到 6 参。命令路径(SqlExecuteCommandFunction 第 59 行)调用的是 6 参,因此外部子类若只覆写 5 参(本 PR 之前唯一的可覆写入口),其自定义行读取/转换逻辑会被静默绕过。仓库内未发现此类子类,但废弃入口本轮也转到了这条路径(见下一条),影响面比上轮更大。建议:套用 CommonDbConnector 的覆写探测——子类只覆写 5 参且超时为 0 时回调 5 参,超时为正值时抛 UnsupportedOperationException,并补一条"只覆写 5 参的子类仍生效"的测试。

[中] connectors-common/sql-core/src/main/java/io/tapdata/common/SqlExecuteCommandFunction.java:42
废弃 executeCommand 改走流式路径,结果回调契约变了(空结果集不再回调)
改动前:sqlExecutor.execute(sql, connectionSupplier),每条命令恰好回调一次——SELECT 回调一次完整 List<DataMap>(空结果为空列表),出错回调 error 结果。改动后走 6 参流式路径:①结果按 batchSize(默认 1000)拆成多次回调;②JdbcQueryExecutor 第 58 行的 if (!batch.isEmpty()) 使得空 SELECT 一次回调都不发,等待"恰好一个结果"的旧调用方会挂起或误判为无返回;③多结果集全部推出,原先只取第一个;④自定义 SqlExecutor 里覆写的废弃 2 参 execute 被绕过。仓库内 Hive/Hive1/Hudi/Databend/Snowflake 都走 aliveSupplier 重载不受影响,受影响的是外部或遗留调用方。新测试 deprecatedCommandAlsoPassesTimeoutToJdbc 只走了 DML(statement.execute 返回 false)路径,空 SELECT 和多批次均未覆盖。建议:废弃入口保留单结果路径,超时为 0 时行为完全不变,为正值时在旧路径里调用 setQueryTimeout 或明确报错;并补空结果集与多批次两条回归测试。

[低] connectors/paimon-plus-connector/src/test/java/io/tapdata/connector/paimon/service/PaimonQueryPlanTest.java:263
超时用例的新断言测不出"排序中检查"
PaimonQueryLimits 构造函数里 started = clock.getAsLong() 已经读过一次时钟,所以 assertTrue(ticks.get() > 0) 恒真。按 +400ms 推演:finish 入口 check(0.4s)、pre-sort check(0.8s)后进入排序,比较器采样那次读到 1.2s 才到期;但即便删掉比较器里的检查,排序结束后的 limits.check() 同样会在 1.2s 抛 SQLTimeoutException,用例照样通过——用例名声称的"排序内检查截止时间"没有被验证。建议:让超时恰好在比较器采样那次到期(例如前几次 check 不推进时钟),并断言异常发生在排序完成之前,或断言 SortCancelled 路径确实被走过。

[低] connectors/mysql-connector/src/main/resources/docs/mysql_en_US.md:87
MySQL 文档建议的超时兜底参数不正确
文档写"配合服务端 socketTimeout/net_read_timeout"来限制逐行读取,但 socketTimeout 是 Connector/J 的客户端连接属性,不是服务端设置;net_read_timeout 控制服务端从客户端读数据,与推送流式结果无关;真正相关的是 net_write_timeout(Connector/J 的 netTimeoutForStreamingResults 即调它)。batchSize 也不提供时间上限。建议:三语文档改为"socketTimeout(Connector/J 客户端属性)/ net_write_timeout(服务端写超时,或连接串 netTimeoutForStreamingResults)",并明确 batchSize 只影响回调粒度。

[低] connectors/paimon-plus-connector/src/main/resources/docs/paimon_zh_TW.md:145
zh_TW 把整型字面量规则放错了段落
"整型欄位只接受欄位範圍內的整數字面量…不會誤報為語法錯誤"被写进了 ORDER BY 那段,而 en_US(:148)与 zh_CN(:161)都放在 WHERE 段。建议:把该句移到下面"WHERE 支援…"那段,使三语结构与内容一致。

[低] connectors-common/sql-core/src/main/java/io/tapdata/common/CommonDbConnector.java:1163
递归守卫会误伤合法的嵌套查询
守卫是线程级标志(1167 行置位、1171 行 remove())。只覆写 3 参的子类若在自己的方法里用 4 参(超时 0)执行另一条查询(例如会话预置语句或预查询),会被判为递归并抛 IllegalStateException,报错信息"must not delegate back"会误导排查方向。建议:把守卫改为"同一 SQL/同一调用栈帧"判定或使用重入计数并在超限时才报错,至少在该方法与 README 中写明"3 参覆写内不得再调用 4 参"的契约。
===END===

标记:❌ 不可通行

if (item instanceof AllColumns && select.getSelectItems().size() == 1) {
plan.star = true;
for (int i = 0; i < rowType.getFieldCount(); i++) {
plan.items.add(new Item(rowType.getFieldNames().get(i), plan.column(i), null, rowType.getFieldNames().get(i)));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 【上轮漏报】SELECT * 遇复杂列由返回整行变为整条命令失败

星号投影对每列调用 column(),而 column() 在 177-178 行拒绝 ARRAY/MAP/MULTISET/ROW,因此含任一复杂列的表执行 SELECT * 会整条失败。基线 executeQuery 走 convertInternalRowToMap:复杂列本身是用 default: row.getString(pos) 转出的无意义值,但同一行里的标量列是可用的;本次改动把"部分可用"变成"完全不可用",属于兼容性行为变更。三语文档虽已写明该限制,但没有任何测试覆盖这条拒绝路径,也没有给出参数化的兼容通道。建议:星号投影只物化标量列并对复杂列给出按列可选的错误/省略策略(或提供 params 开关回到旧行为),同时补一条 "SELECT * 含 ARRAY 列" 的解析测试,并在三语文档写明迁移步骤与错误信息。

@Override
public void execute(String sql, TapSupplier<Connection> connectionSupplier, Consumer<Object> consumer,
Supplier<Boolean> aliveSupplier, int batchSize, int queryTimeoutSeconds) throws Throwable {
JdbcQueryExecutor.execute(sql, connectionSupplier, consumer, aliveSupplier, batchSize, queryTimeoutSeconds,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 【上轮漏报】只覆写 5 参 execute 的 DefaultSqlExecutor 子类会被命令路径绕过

6 参方法直接调用 JdbcQueryExecutor.execute(52-60 行),不会分派到 5 参方法;5 参(44-47 行)只是委派到 6 参。命令路径(SqlExecuteCommandFunction 第 59 行)调用的是 6 参,因此外部子类若只覆写 5 参(本 PR 之前唯一的可覆写入口),其自定义行读取/转换逻辑会被静默绕过。仓库内未发现此类子类,但废弃入口本轮也转到了这条路径(见下一条),影响面比上轮更大。建议:套用 CommonDbConnector 的覆写探测——子类只覆写 5 参且超时为 0 时回调 5 参,超时为正值时抛 UnsupportedOperationException,并补一条"只覆写 5 参的子类仍生效"的测试。

executeResultConsumer.accept(executeResult);
// Keep the deprecated entry point on the same code path so queryTimeoutSeconds and batchSize
// are honored; legacy callers have no cancellation supplier, so the command never cancels.
executeCommand(sqlExecutor, tapExecuteCommand, connectionSupplier, () -> true, executeResultConsumer);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] 废弃 executeCommand 改走流式路径,结果回调契约变了(空结果集不再回调)

改动前:sqlExecutor.execute(sql, connectionSupplier),每条命令恰好回调一次——SELECT 回调一次完整 List<DataMap>(空结果为空列表),出错回调 error 结果。改动后走 6 参流式路径:①结果按 batchSize(默认 1000)拆成多次回调;②JdbcQueryExecutor 第 58 行的 if (!batch.isEmpty()) 使得空 SELECT 一次回调都不发,等待"恰好一个结果"的旧调用方会挂起或误判为无返回;③多结果集全部推出,原先只取第一个;④自定义 SqlExecutor 里覆写的废弃 2 参 execute 被绕过。仓库内 Hive/Hive1/Hudi/Databend/Snowflake 都走 aliveSupplier 重载不受影响,受影响的是外部或遗留调用方。新测试 deprecatedCommandAlsoPassesTimeoutToJdbc 只走了 DML(statement.execute 返回 false)路径,空 SELECT 和多批次均未覆盖。建议:废弃入口保留单结果路径,超时为 0 时行为完全不变,为正值时在旧路径里调用 setQueryTimeout 或明确报错;并补空结果集与多批次两条回归测试。

assertTrue(failure.getMessage().contains("queryTimeoutSeconds=1"));
// The exact number of clock reads is an implementation detail (the sort samples the
// deadline); only require that the deadline was actually polled.
assertTrue(ticks.get() > 0);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 超时用例的新断言测不出"排序中检查"

PaimonQueryLimits 构造函数里 started = clock.getAsLong() 已经读过一次时钟,所以 assertTrue(ticks.get() > 0) 恒真。按 +400ms 推演:finish 入口 check(0.4s)、pre-sort check(0.8s)后进入排序,比较器采样那次读到 1.2s 才到期;但即便删掉比较器里的检查,排序结束后的 limits.check() 同样会在 1.2s 抛 SQLTimeoutException,用例照样通过——用例名声称的"排序内检查截止时间"没有被验证。建议:让超时恰好在比较器采样那次到期(例如前几次 check 不推进时钟),并断言异常发生在排序完成之前,或断言 SortCancelled 路径确实被走过。


### Custom SQL command timeout

`execute` / `executeQuery` command parameters: `sql` is the SQL text; `batchSize` is a whole int (default 1000; zero or negative values use the default); `queryTimeoutSeconds` is whole seconds in 0..2147483647. Omitted or 0 means no statement timeout; negative, fractional and overflowing values are rejected. Positive values use JDBC `Statement.setQueryTimeout`. A driver that cannot enforce it returns an explicit error; set 0 only if an unlimited query is acceptable. MySQL command queries read the result set in streaming mode (`fetchSize=Integer.MIN_VALUE`); Connector/J ties the cancel timer to statement execution, so the timeout bounds `execute()` but not the subsequent row-by-row fetch. Use `batchSize`, cancellation and a server-side `socketTimeout`/`net_read_timeout` if row reading itself must be bounded.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] MySQL 文档建议的超时兜底参数不正确

文档写"配合服务端 socketTimeout/net_read_timeout"来限制逐行读取,但 socketTimeout 是 Connector/J 的客户端连接属性,不是服务端设置;net_read_timeout 控制服务端从客户端读数据,与推送流式结果无关;真正相关的是 net_write_timeout(Connector/J 的 netTimeoutForStreamingResults 即调它)。batchSize 也不提供时间上限。建议:三语文档改为"socketTimeout(Connector/J 客户端属性)/ net_write_timeout(服务端写超时,或连接串 netTimeoutForStreamingResults)",并明确 batchSize 只影响回调粒度。


### SQL 邊界與回傳類型

支援 `SELECT *`、標量欄位投影與別名、`COUNT(*)`、`COUNT(1)`、`COUNT(column)`、`SUM`、`AVG`、`MIN`、`MAX`、多欄位 GROUP BY,以及依已選擇的表達式、別名或序號 ORDER BY。`CAST(column AS DATE)` 僅接受 DATE 或無時區 TIMESTAMP,直接取日曆日期,不隱式轉換時區;不支援 DATE(column)。ASC 預設 NULLS FIRST,DESC 預設 NULLS LAST,也可明確指定。排序需將結果行快取在記憶體,超過 `maxBufferedRows` 即整次失敗;整型欄位只接受欄位範圍內的整數字面量,小數或超範圍字面量會以 `literal ... is not representable as ...` 明確報錯,不會靜默截斷,也不會誤報為語法錯誤。ORDER BY 未加引號的別名忽略大小寫,帶引號的別名精確匹配。目前 JSqlParser 4.4 不接受識別字內部雙寫雙引號的語法。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] zh_TW 把整型字面量规则放错了段落

"整型欄位只接受欄位範圍內的整數字面量…不會誤報為語法錯誤"被写进了 ORDER BY 那段,而 en_US(:148)与 zh_CN(:161)都放在 WHERE 段。建议:把该句移到下面"WHERE 支援…"那段,使三语结构与内容一致。

}
// Preserve a subclass that only customizes the three-argument overload. A subclass whose
// three-argument override delegates back here would recurse forever, so fail loudly.
if (Boolean.TRUE.equals(LEGACY_DELEGATION.get())) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 递归守卫会误伤合法的嵌套查询

守卫是线程级标志(1167 行置位、1171 行 remove())。只覆写 3 参的子类若在自己的方法里用 4 参(超时 0)执行另一条查询(例如会话预置语句或预查询),会被判为递归并抛 IllegalStateException,报错信息"must not delegate back"会误导排查方向。建议:把守卫改为"同一 SQL/同一调用栈帧"判定或使用重入计数并在超限时才报错,至少在该方法与 README 中写明"3 参覆写内不得再调用 4 参"的契约。

@jarad0628 jarad0628 closed this Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant