This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch branch-catalog-spi
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-catalog-spi by this
push:
new 778c5dd610f [P1-T03-T05] route plugin-driven scans first in nereids
translator (#63641)
778c5dd610f is described below
commit 778c5dd610fb25336b1cad52297da7c61f400d43
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon May 25 11:45:04 2026 -0700
[P1-T03-T05] route plugin-driven scans first in nereids translator (#63641)
## Summary
P1 batch A — close out scan-node SPI consolidation while keeping
migration-period fallbacks in place. Three surgical changes route
`PluginDrivenExternalTable` first in the nereids translator hot paths so
already-migrated SPI connectors (JDBC, ES) take the SPI route, while the
existing `instanceof XExternalTable` chains remain as fallbacks for
connectors still pending migration (P3–P7).
- **T3** — `PhysicalPlanTranslator.visitPhysicalFileScan`: move the
existing `PluginDrivenExternalTable` branch from position 8 to position
1; the 7 connector-specific branches (HMS / Iceberg / Paimon / Trino /
MaxCompute / LakeSoul / RemoteDoris) stay in place as migration-period
fallbacks
- **T4** — `PhysicalPlanTranslator.visitPhysicalHudiScan`: add a
`PluginDrivenExternalTable` branch routed to
`PluginDrivenScanNode.create(...)`, threading `tableSnapshot` +
`scanParams` through `FileQueryScanNode` setters; `incrementalRelation`
flagged as a P3 Hudi SPI extension TODO. The new branch is unreachable
today (`PhysicalHudiScan` is only built for `HMSExternalTable +
DLAType.HUDI`), so this is groundwork for P3 with zero current-day
runtime impact
- **T5** — `LogicalFileScan`: in `computeOutput()`, add a
`PluginDrivenExternalTable` branch calling new helper
`computePluginDrivenOutput()` — same shape as `computeIcebergOutput`,
using `getFullSchema()` + virtualColumns; in
`supportPruneNestedColumn()`, add an explicit `PluginDrivenExternalTable
→ false` branch. Both behaviorally equivalent for JDBC/ES today since
they have no hidden cols and no virtualColumns
P1 batch B (T1 — delete 13 legacy `Jdbc*Client` + `JdbcFieldSchema`) is
deferred to P8 because the 3 fe-core callers —
`PostgresResourceValidator`, `StreamingJobUtils`,
`CdcStreamTableValuedFunction` — are live CDC streaming code that
requires SPI extension for `getPrimaryKeys` / `getColumnsFromJdbc` /
`listTables`, which is out of P1 surgical scope.
Background and tracking docs live in `plan-doc/` (Master Plan §3.2 P1,
tasks/P1-scan-node-cleanup.md, decisions log).
## Test plan
- [x] `mvn -pl fe-core -am compile -Dmaven.build.cache.enabled=false` →
BUILD SUCCESS
- [x] `mvn -pl fe-core checkstyle:check` → 0 violations
- [x] JDBC + ES regression-test passing — baseline established in P0 /
PR #63582
- [ ] PR CI green on this PR
- [ ] Manual scan-node smoke for an SPI connector — JDBC `SELECT *`
should fall into the new `PluginDrivenExternalTable` branch first
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <[email protected]>
---
.../glue/translator/PhysicalPlanTranslator.java | 43 +++--
.../trees/plans/logical/LogicalFileScan.java | 25 +++
plan-doc/HANDOFF.md | 198 ++++++++++++---------
plan-doc/PROGRESS.md | 32 ++--
plan-doc/tasks/P1-scan-node-cleanup.md | 137 ++++++++++++++
5 files changed, 326 insertions(+), 109 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
index 7b01ce6c6b1..1eaef6f98a7 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
@@ -731,7 +731,16 @@ public class PhysicalPlanTranslator extends
DefaultPlanVisitor<PlanFragment, Pla
SessionVariable sv = ConnectContext.get().getSessionVariable();
// TODO(cmy): determine the needCheckColumnPriv param
ScanNode scanNode;
- if (table instanceof HMSExternalTable) {
+ // Plugin-driven (SPI) tables are matched first; the connector-specific
+ // instanceof branches below are migration-period fallbacks that get
removed
+ // as each connector lands on the SPI in P3-P7.
+ if (table instanceof PluginDrivenExternalTable) {
+ PluginDrivenExternalCatalog pluginCatalog =
+ (PluginDrivenExternalCatalog) table.getCatalog();
+ scanNode = PluginDrivenScanNode.create(context.nextPlanNodeId(),
tupleDescriptor,
+ false, sv, context.getScanContext(), pluginCatalog,
+ ((PluginDrivenExternalTable) table));
+ } else if (table instanceof HMSExternalTable) {
if (directoryLister == null) {
this.directoryLister = new
TransactionScopeCachingDirectoryListerFactory(
Config.max_external_table_split_file_meta_cache_num).get(new
FileSystemDirectoryLister());
@@ -779,12 +788,6 @@ public class PhysicalPlanTranslator extends
DefaultPlanVisitor<PlanFragment, Pla
} else if (table instanceof RemoteDorisExternalTable) {
scanNode = new RemoteDorisScanNode(context.nextPlanNodeId(),
tupleDescriptor, false, sv,
context.getScanContext());
- } else if (table instanceof PluginDrivenExternalTable) {
- PluginDrivenExternalCatalog pluginCatalog =
- (PluginDrivenExternalCatalog) table.getCatalog();
- scanNode = PluginDrivenScanNode.create(context.nextPlanNodeId(),
tupleDescriptor,
- false, sv, context.getScanContext(), pluginCatalog,
- ((PluginDrivenExternalTable) table));
} else {
throw new RuntimeException("do not support table type " +
table.getType());
}
@@ -819,19 +822,35 @@ public class PhysicalPlanTranslator extends
DefaultPlanVisitor<PlanFragment, Pla
@Override
public PlanFragment visitPhysicalHudiScan(PhysicalHudiScan hudiScan,
PlanTranslatorContext context) {
- if (directoryLister == null) {
- this.directoryLister = new
TransactionScopeCachingDirectoryListerFactory(
-
Config.max_external_table_split_file_meta_cache_num).get(new
FileSystemDirectoryLister());
- }
List<Slot> slots = hudiScan.getOutput();
ExternalTable table = hudiScan.getTable();
TupleDescriptor tupleDescriptor = generateTupleDesc(slots, table,
context);
+ SessionVariable sv = ConnectContext.get().getSessionVariable();
+
+ // Plugin-driven (SPI) Hudi: route through PluginDrivenScanNode.
Incremental scan
+ // (hudiScan.getIncrementalRelation) is not yet representable in the
SPI; that
+ // gap is tracked for P3 when Hudi migrates to the connector framework.
+ if (table instanceof PluginDrivenExternalTable) {
+ PluginDrivenExternalCatalog pluginCatalog =
+ (PluginDrivenExternalCatalog) table.getCatalog();
+ ScanNode scanNode =
PluginDrivenScanNode.create(context.nextPlanNodeId(), tupleDescriptor,
+ false, sv, context.getScanContext(), pluginCatalog,
+ (PluginDrivenExternalTable) table);
+ FileQueryScanNode fileScan = (FileQueryScanNode) scanNode;
+
hudiScan.getTableSnapshot().ifPresent(fileScan::setQueryTableSnapshot);
+ hudiScan.getScanParams().ifPresent(fileScan::setScanParams);
+ return getPlanFragmentForPhysicalFileScan(hudiScan, context,
scanNode);
+ }
+ if (directoryLister == null) {
+ this.directoryLister = new
TransactionScopeCachingDirectoryListerFactory(
+
Config.max_external_table_split_file_meta_cache_num).get(new
FileSystemDirectoryLister());
+ }
if (!(table instanceof HMSExternalTable) || ((HMSExternalTable)
table).getDlaType() != DLAType.HUDI) {
throw new RuntimeException("Invalid table type for Hudi scan: " +
table.getType());
}
HudiScanNode hudiScanNode = new HudiScanNode(context.nextPlanNodeId(),
tupleDescriptor, false,
- hudiScan.getScanParams(), hudiScan.getIncrementalRelation(),
ConnectContext.get().getSessionVariable(),
+ hudiScan.getScanParams(), hudiScan.getIncrementalRelation(),
sv,
directoryLister, context.getScanContext());
if (hudiScan.getTableSnapshot().isPresent()) {
hudiScanNode.setQueryTableSnapshot(hudiScan.getTableSnapshot().get());
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
index f34ea0d633d..8ca7902a402 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
@@ -22,6 +22,7 @@ import org.apache.doris.analysis.TableSnapshot;
import org.apache.doris.catalog.PartitionItem;
import org.apache.doris.common.IdGenerator;
import org.apache.doris.datasource.ExternalTable;
+import org.apache.doris.datasource.PluginDrivenExternalTable;
import org.apache.doris.datasource.hive.HMSExternalTable;
import org.apache.doris.datasource.iceberg.IcebergExternalTable;
import org.apache.doris.datasource.iceberg.IcebergSysExternalTable;
@@ -203,6 +204,12 @@ public class LogicalFileScan extends
LogicalCatalogRelation implements SupportPr
return cachedOutputs.get();
}
+ if (table instanceof PluginDrivenExternalTable) {
+ // SPI-driven tables: schema is fetched via
ConnectorMetadata.getTableSchema()
+ // (see PluginDrivenExternalTable.initSchema). Use getFullSchema()
so any
+ // hidden/metadata columns the connector exposes are reachable.
+ return computePluginDrivenOutput();
+ }
if (table instanceof IcebergExternalTable) {
// iceberg v3 need append row lineage columns
return computeIcebergOutput((IcebergExternalTable) table);
@@ -225,6 +232,19 @@ public class LogicalFileScan extends
LogicalCatalogRelation implements SupportPr
return slots.build();
}
+ private List<Slot> computePluginDrivenOutput() {
+ IdGenerator<ExprId> exprIdGenerator =
StatementScopeIdGenerator.getExprIdGenerator();
+ Builder<Slot> slots = ImmutableList.builder();
+ table.getFullSchema()
+ .stream()
+ .map(col ->
SlotReference.fromColumn(exprIdGenerator.getNextId(), table, col, qualified()))
+ .forEach(slots::add);
+ for (NamedExpression virtualColumn : virtualColumns) {
+ slots.add(virtualColumn.toSlot());
+ }
+ return slots.build();
+ }
+
@Override
public List<Slot> computeAsteriskOutput() {
return super.computeAsteriskOutput();
@@ -233,6 +253,11 @@ public class LogicalFileScan extends
LogicalCatalogRelation implements SupportPr
@Override
public boolean supportPruneNestedColumn() {
ExternalTable table = getTable();
+ if (table instanceof PluginDrivenExternalTable) {
+ // No SPI capability for nested-column prune yet; default to off.
+ // Future ConnectorCapability flag will refine this.
+ return false;
+ }
if (table instanceof IcebergExternalTable || table instanceof
IcebergSysExternalTable) {
return true;
} else if (table instanceof HMSExternalTable) {
diff --git a/plan-doc/HANDOFF.md b/plan-doc/HANDOFF.md
index 9219adff17d..cdc3c4f7b74 100644
--- a/plan-doc/HANDOFF.md
+++ b/plan-doc/HANDOFF.md
@@ -8,139 +8,164 @@
## 📅 最后一次 handoff
-- **日期 / 时间**:2026-05-24(夜 ③)
+- **日期 / 时间**:2026-05-25(白天 ④)
- **本 session 主导者**:Claude Opus 4.7(1M context)
-- **本 session 主题**:P0 批 2 守门 + 单测(T21-T23, T26-T27;T24-T25 转交用户在本地跑)—— **已
commit**(用户人工 review 通过;hash 见 `git log --oneline -3`,subject
`[feat](connector) add P0 batch 2 gate + unit tests (T21-T23, T26-T27)`)
-- **预估 context 使用**:~55%(健康)
+- **本 session 主题**:**P1 阶段关闭**(批 B = T1 推迟到 P8;in-scope 100% 完成)
+- **预估 context 使用**:~25%(健康;本场无编码,主要是 recon + 用户决议 + 跟踪文档同步)
---
## ✅ 本 session 完成项
-### 1. P0 批 2:守门 + 单测(T21-T23, T26-T27)
+### 1. 批 B (T1) recon — 揭示 callers 非 dead code
-| ID | 任务 | 文件 | 备注 |
-|---|---|---|---|
-| T21 ✅ | `tools/check-connector-imports.sh` | **新**
`tools/check-connector-imports.sh` | grep 守门;接受可选 ROOT 参数;正负冒烟均通过 |
-| T22 ✅ | exec-maven-plugin 接入脚本 | edit `fe-connector/pom.xml` | 绑 `validate`
阶段;`inherited=false`;用 `${project.basedir}/../../tools/...` 避开 `fe.dir` 解析时序 |
-| T23 ✅ | `FakeConnectorPlugin` + 默认行为测试 | **新**
`fe-core/src/test/java/.../connector/fake/{FakeConnectorPlugin,FakeConnectorPluginTest}.java`
| 11 个 @Test;零 override 的 `FakeMetadata` 验证所有 default 路径 |
-| T24 ⏳ | JDBC regression-test | — | **转交用户**在本地跑 |
-| T25 ⏳ | ES regression-test | — | **转交用户**在本地跑 |
-| T26 ✅ | `ExternalMetaCacheInvalidator` 路由测试 | **新**
`fe-core/src/test/java/.../connector/ExternalMetaCacheInvalidatorTest.java` | 5
个 @Test;`MockedStatic<Env>` + `mock(ExternalMetaCacheMgr)`;pin partition
fallback & stats no-op |
-| T27 ✅ | converter 单测 | **新**
`fe-core/src/test/java/.../connector/ddl/CreateTableInfoToConnectorRequestConverterTest.java`
| 7 个 @Test;`mock(CreateTableInfo)` 绕开 18-arg ctor;4 partition style + 2
bucket + 列穿透 |
+启动批 B 前对 `Jdbc*Client.java` + `JdbcFieldSchema.java` 的 fe-core 引用做了 Explore
subagent 调研。结论:
-### 2. 验证
+| Caller(路径) | Live? | 用途 |
+|---|---|---|
+| `job/extensions/insert/streaming/PostgresResourceValidator.java` | ✅ 活 |
CREATE JOB 时校验 PG 复制槽 / 发布;被 StreamingJobUtils → StreamingInsertJob →
CreateJobCommand 链调用 |
+| `job/util/StreamingJobUtils.java` | ✅ 活 | `getJdbcClient()` +
`getPrimaryKeys`/`getColumnsFromJdbc`/`getTablesNameList`,CDC 表枚举 + DDL 生成 |
+| `tablefunction/CdcStreamTableValuedFunction.java` | ✅ 活 | `cdc_stream` TVF,被
`CdcStream.java:46` 调,streaming 作业执行链路 |
-- `tools/check-connector-imports.sh` 正/负冒烟测试通过
-- `mvn -pl fe-connector validate -Dmaven.build.cache.enabled=false` → **BUILD
SUCCESS**(exec-maven-plugin 调起脚本)
-- `mvn -pl fe-core -am test
-Dtest='FakeConnectorPluginTest,ExternalMetaCacheInvalidatorTest,CreateTableInfoToConnectorRequestConverterTest,ConnectorPluginManagerTest,ConnectorSessionImplTest'
-DfailIfNoTests=false -Dmaven.build.cache.enabled=false` → **39/39 tests
green**
-- `mvn -pl fe-core checkstyle:check` → **0 violations**
+测试侧:`StreamingJobUtilsTest`(需重写);`JdbcFieldSchemaTest` /
`JdbcClickHouseClientTest` / `JdbcClientExceptionTest`(测 legacy 本身,随源删除)。
-### 3. 文档同步(§5.1 五步纪律)
+fe-connector 侧 SPI 替换
`Jdbc*ConnectorClient`(ClickHouse/DB2/MySQL/Oracle/PostgreSQL/SQLServer/SapHana/Gbase)已就位,但
**fe-core 不能直接 import** —— 会破坏 `tools/check-connector-imports.sh` 守门。
-- ✅ `tasks/P0-spi-foundation.md`:T21-T23, T26-T27 状态翻 ✅;T24-T25 owner 改 @用户;新增
2026-05-24(夜 ③)日志条目(含 4 项 trade-off 说明);顶部验收清单 5 项翻 [x]
-- ✅ `PROGRESS.md`:§一 P0 进度条 74% → 93%;§三 P0 表追加批 2 7 行;§四加 2026-05-24(夜
③)条目;§七 session 状态滚动
-- ✅ 本 HANDOFF.md 覆写
-- N/A `connectors/<name>.md`(本场不属任何具体连接器)
-- N/A `decisions-log.md` / `deviations-log.md`(trade-off 都在 RFC §15 范围内,未升 DV)
+### 2. 用户决议(Q4):推迟 T1 到 P8 收尾
-### 4. Commit(用户人工 review 通过后)
+- 删 T1 需要在 `ConnectorPlugin`/`ConnectorMetadata` 上为 CDC use case 暴露
`getPrimaryKeys` / `getColumnsFromJdbc` / `listTables` 新 capability — 是 SPI
扩展工作,超出 Master Plan §3.2 P1 scope
+- 现状无 runtime 风险——legacy JDBC client 仍在原位,CDC 功能正常
+- 决策:T1 推迟到 P8 收尾,与 streaming CDC 重构一起做(避免 P1 阶段引入 1-2 天计划外 SPI 设计)
-- ✅ `[feat](connector) add P0 batch 2 gate + unit tests (T21-T23,
T26-T27)`(hash 见 `git log --oneline -3`)
-- 9 files changed:1 个 pom edit(fe-connector)+ 5 个新文件(1 脚本 + 4 测试相关)+ 3 个
plan-doc 更新
-- 工作树 clean
+P1 状态因此提前关闭:**in-scope (T3+T4+T5) 100% 完成;T1 推迟 P8;T2 推迟 P4/P5**。
+
+### 3. 跟踪文档同步
+
+- `tasks/P1-scan-node-cleanup.md`:元信息状态翻 ✅;验收标准重新对齐(标 🚫/[x]/🟡);任务表 T1 翻 🚫 +
备注引用 Q4;新增 白天 ④ 阶段日志条目;当前阻塞项更新
+- `PROGRESS.md`:header 项目总进度 16% → 20%;§一 P1 → 100% ✅;§一 P2 → 🚧 准备启动;全局进度 8% →
12%;§三 P1 表 header 改 "✅ 已完成",T1 行翻 🚫;§四加 白天 ④ 条目;§七 session 状态更新
+- `HANDOFF.md`(本文件):覆盖更新到 P1 阶段关闭状态
---
## 🚧 本 session 进行中 / 未完成
-- **T24/T25**:JDBC + ES regression-test 转交用户在本地跑(containers / docker
在本地更稳)。任务状态保持 ⏳,owner 改为 @用户。完成后用户在 PROGRESS / tasks 上翻 ✅ 即可
-- **本 HANDOFF 在 commit 内**——内容写的是 post-commit 状态,与 batch 2 代码、plan-doc 更新一并
commit。不需要后续 amend
+无编码工作。剩余动作:
+
+1. **commit 本场 plan-doc 改动** — 3 个文件(P1 task / PROGRESS / HANDOFF)
+2. **push `catalog-spi-02` 到 morningman fork**(**待用户授权**)— 含批 A commit
`43a12a05ffe` + 本场 doc commit
+3. **`gh pr create --repo apache/doris --base branch-catalog-spi --head
morningman:catalog-spi-02`**(**待用户授权**)
---
## 📝 关键认知 / 临时发现
-继承上版认知不变。**本场新增**:
+继承前一版认知。**本场新增**:
-1. **maven-enforcer-plugin 不能原生 exec shell**——RFC §15.4 原文写"挂到 maven enforcer
plugin",但 enforcer 只有 `requireXxx` 系列 rule 和 `EvaluateBeanshell`,没有内置的
shell-exec rule。要么写 Java 自定义 Rule 类(重)要么走 `EvaluateBeanshell`(不直观)。**最终选择
`exec-maven-plugin`**——fe-common 已用它跑 make + protoc,零新依赖;脚本 non-zero exit 即触发
`BUILD FAILURE`,效果等价
-2. **`directory-maven-plugin` 的 `fe.dir` 属性在 `validate` 阶段还没 set**——它绑
`initialize` 阶段(晚于 validate)。第一次写 pom 用了
`${doris.home}/tools/...`(`doris.home=${fe.dir}/../`),结果路径解析为字面值
`${fe.dir}/..//tools/...`。改用 `${project.basedir}/../../tools/...`(fe-connector
aggregator basedir → workspace root → tools)避开属性时序问题
-3. **exec-maven-plugin 在 aggregator pom 的继承默认是 `inherited=true`**——会让 11 个
fe-connector-* 子模块每次都重跑同一份扫描。本场设 `inherited=false`,只在 aggregator 自身 lifecycle
跑一次。Trade-off:dev 跑单个子模块 `mvn -pl fe-connector/fe-connector-iceberg compile`
时不会自动触发守门,但顶层 `mvn install` 必扫
-4. **`ConnectorMetaInvalidator` 的方法名是 `invalidateAll()` 不是
`invalidateCatalog()`**——第一稿测试写错卡了一次 test-compile。SPI 接口侧明确写
`invalidateAll`("Invalidates the entire catalog's metadata caches"),fe-core 侧
`ExternalMetaCacheInvalidator.invalidateAll() →
mgr.invalidateCatalog(catalogId)` 这才是路由
-5. **`Mockito.mockStatic(Env.class)`** 模式在 fe-core
已有先例(`BDBDebuggerTest:115`),mockito-inline 是 fe 顶层 pom 已声明的 test
dep,新测试可以直接用,无需修改任何 pom
-6. **`Mockito.mock(CreateTableInfo.class)`** 比真正构造 18-arg `CreateTableInfo`
更便捷——converter 只读 8 个 getter,全部 stub 即可。如未来 converter 用到更多 getter,在 `stubInfo`
helper 加新 stub
-7. **`mvn -pl fe-core test` 不带 `-am` 失败**(缺 fe-grpc / fe-filesystem-* 等本地未
install 的 SNAPSHOT)。本场所有 fe-core 测试运行都用 `mvn -pl fe-core -am test -Dtest=...
-DfailIfNoTests=false
-Dmaven.build.cache.enabled=false`。`-DfailIfNoTests=false` 是必须的——`-am` 会带上
fe-foundation 等 upstream,它们没有匹配 `-Dtest=` 的测试就会爆 surefire 错
-8. **fe-connector 模块当前 import 现状**:`grep -rEn "^import org\.apache\.doris\."
fe/fe-connector/*/src/main/java | awk` → 仅 4 个根包 `connector / extension /
thrift /
trinoconnector`。所有禁词包(catalog/common/datasource/qe/analysis/nereids/planner)都被守门,baseline
已经合规
+1. **`tools/check-connector-imports.sh` 是一个隐含的设计约束** — fe-core 不能 import
fe-connector 内部类(`org.apache.doris.connector.*`),所以"复用"SPI 实现唯一通道是
`ConnectorPlugin` 接口。批 B 直接 import `JdbcConnectorClient` 替换 `JdbcClient`
本能解法**走不通**——一定要经过 SPI capability 扩展。这条约束以前 P0 文档讲过,但批 B recon 时是第一次真正触发它
+2. **CDC streaming 是 SPI 未覆盖的 use case** — 现有 SPI(ConnectorMetadata.getTable /
listTables / getTableHandle)是面向"标准 SELECT"的,没暴露 PK
探测、columns-from-jdbc-driver、replication-slot 校验。P8 启动前需要先在 RFC 中起 §17
章节描述这套扩展,否则 P8 实施会 stall
+3. **fe-connector 侧的 `Jdbc*ConnectorClient` 是 P0 阶段 JDBC 迁移的产物** — 它们没有暴露 PK /
column-from-driver 接口(按 ConnectorMetadata 标准抽象设计),所以即便允许 fe-core 直接 import
也不能直接替换 legacy client。换言之 SPI 设计本身需要扩展(不只是 "改 import 路径")
---
## 🎯 下一个 session 第一件事
-### Track A:等 T24/T25 收尾
+> P1 已关闭。下一阶段 P2 (trino-connector,2 周)。**预备动作**:先把批 A push + PR,再做 P2 recon。
```
-1. 用户跑完 JDBC + ES regression-test 后
-2. tasks/P0-spi-foundation.md 把 T24/T25 翻 ✅
-3. PROGRESS.md 进度条 93% → 100%;状态 🚧 → ✅
-4. 写 P0 阶段收尾 commit(如果 T24/T25 有微调代码)
+1. git branch --show-current → 确认在 catalog-spi-02
+ git status → 应 clean(本场 doc commit 已 push 前提下)
+ git log --oneline -3 → 应见 2 个本地未推 commit:
+ a) 批 A scan-node 收口(43a12a05ffe)
+ b) P1 关闭 + T1 推迟 P8 doc commit
+2. 读 PROGRESS.md + 本 HANDOFF + tasks/P1-scan-node-cleanup.md(确认 P1 已 ✅)
+3. push + PR(如本场尚未完成):
+ git push -u origin catalog-spi-02
+ gh pr create --repo apache/doris --base branch-catalog-spi \
+ --head morningman:catalog-spi-02 \
+ --title "[P1-T03-T05] route plugin-driven scans first in nereids
translator"
+4. 启动 P2 (trino-connector) recon — 用 Explore subagent:
+ a. fe-core 侧 `datasource/trinoconnector/` 现状(多少类、多少 LOC)
+ b. fe-connector 侧 trino-connector 模块完成度(连接器看板里目前标 70%)
+ c. SPI_READY 加进 `CatalogFactory.SPI_READY_TYPES` 的预条件
+ d. 反向 instanceof:grep "instanceof.*Trino" in nereids/planner(看板里目前标 0/2)
+5. 创建 plan-doc/tasks/P2-trino-connector-migration.md(_template.md 复制)
+6. 守门:P2 改动跨 fe-core + fe-connector 双侧,每次 commit 前
+ - `mvn -pl fe-connector validate` 触发 check-connector-imports.sh
+ - `mvn -pl fe-core checkstyle:check`
```
-### Track B:选 P0 末加项 vs 直接进 P1
+---
+
+## ⚠️ 开放问题 / 风险提示
-- **选项 B1**:P0-T28 benchmark(R-006 缓解,1k catalog × `listTableNames` 性能基线)。原列入
P1,可前置到 P0 末加,让 P0 出阶段干净
-- **选项 B2**:直接进 P1(scan-node 收口 + 重复清理)。P0 既然 93% 接近收尾,T24/T25 跑完即关阶段
-- 推荐 B2(B1 在 P1 阶段开题更自然,benchmark 跟 scan-node 工作正好同期)
+继承前一版;批 B 关闭 1 项、转入 P8 待办 1 项;其余沿用。
-### ~~Track C:commit 批 2~~(已收尾)
+### 本场关闭
-批 2 已合入 `catalog-spi-00`;无需再开 Track C。
+- ~~T1 何时实施~~ — 已决:推迟 P8 收尾
----
+### 本场新增(P8 待办)
-## ⚠️ 开放问题 / 风险提示
+1. **P8 SPI 扩展:CDC capability 群**:为 streaming CDC 在 SPI 上暴露 `getPrimaryKeys` /
`getColumnsFromJdbc` / `listTables`(候选:`ConnectorMetadata` 新 default 方法 + 或
`ConnectorPlugin` 上的 `Optional<ConnectorCdcSupport>`);改写
PostgresResourceValidator / StreamingJobUtils / CdcStreamTableValuedFunction 走
SPI;重写 StreamingJobUtilsTest;批量删 13 个 Jdbc*Client + JdbcFieldSchema + 3 个
legacy test。**预估**:~1-2 天 SPI 设计 + ~1 天实施
+2. **P8 启动前 RFC 扩展**:在 `01-spi-extensions-rfc.md` 新增 §17 章节描述 CDC capability
设计;否则 P8 实施会 stall
-继承上版 7 项不变(删了"未 commit batch 1"项;增加本场 trade-off):
-
-1. **守门挂 `exec-maven-plugin` 而非 `maven-enforcer-plugin`**:RFC §15.4
原文写后者。本场用前者(等价实现,0 新依赖)。是否在 RFC §15.4 加脚注说明这个偏差?**判断**:trade-off 在 RFC 范围内,不升
DV;若有 reviewer 强烈要求 enforcer 写 Java Rule 类再重做
-2. **守门 `inherited=false`**:dev 跑单连接器 `mvn -pl
fe-connector/fe-connector-iceberg compile` 时不会触发。是否要改
`inherited=true`?**判断**:现状没人手动跑这条命令日常迭代,重复扫的成本(11 × ~50ms)也不大;如未来某个连接器开发体感差再改
-3. **`invalidatePartition` 测试 pin 当前 fallback**:一旦 SPI 在该方法签名上加 column
名携带能力,bridge 和测试必须同步更新。测试已留 inline comment 描述意图
-4. **`CreateTableInfo` 用 mock**:converter 改用 mock 之外的 getter 时,需在 `stubInfo`
helper 加新 stub。Trade-off:测试更聚焦但代价是输入对象不"真实"
-5. **partition 风格的 IDENTITY vs TRANSFORM 判别**:测试覆盖了"全 UnboundSlot →
IDENTITY"和"含 UnboundFunction → TRANSFORM"两路径,但没覆盖"UnboundSlot + UnboundFunction
混合"——按 converter 当前实现,只要有任意一个 UnboundFunction 就走 TRANSFORM 路径,UnboundSlot 在
`convertFields()` 里也会被识别为 `identity` transform。这个混合场景的语义是否符合预期?**判断**:RFC §4.2
未明确混合用法,留待 P5/P6 Iceberg 真正用到时评估
-6. (沿用)`ColumnDefinition.defaultValue` SPI 缺位
-7. (沿用)LIST/RANGE `initialValues` flatten 缺位
-8. (沿用)`PluginDrivenExternalCatalog.createTable` 返回值丢失"已存在"信息
-9. (沿用)bucket 算法名 `"doris_default"` / `"doris_random"` 占位
-10. (沿用)Maven build cache 误导问题;`mvn -pl fe-core` 必须 cwd=`fe/`
-11. (沿用)`PluginDrivenTransactionManager.begin(ConnectorTransaction)` 暂无 caller
-12. (沿用)`invalidatePartition` fallback;`invalidateStatistics` no-op
-13. (沿用,本场强化)**`mvn -pl fe-core test` 不带 `-am` 失败**:必须 `-am
-DfailIfNoTests=false`
+### 沿用(保留)
+
+3. **T4 PluginDrivenScanNode 不支持 hudi 增量场景** — `incrementalRelation` 待 P3 Hudi
迁移时 SPI 扩展
+4. **T2 已推迟到 P4/P5**(用户决议 Q2,2026-05-25)
+5. **T3 fallback 保留期跨度长**(P1 → P7 20 周)—— 每连接器在 P3-P7 迁移完成后立即删对应 fallback
+6. (沿用 P0)`ColumnDefinition.defaultValue` SPI 缺位 — P5/P6 评估
+7. (沿用 P0)LIST/RANGE `initialValues` flatten 缺位 — P5/P6 评估
+8. (沿用 P0)`PluginDrivenExternalCatalog.createTable` 返回值丢失"已存在"信息 — P5/P6/P7 评估
+9. (沿用 P0)bucket 算法名 `"doris_default"` / `"doris_random"` 占位 — Hive/Iceberg
自己推导
+10. (沿用 P0)Maven build cache 误导;`mvn -pl fe-core` 必须 cwd=`fe/` +
`-am`;`-Dtest=` 务必带 `-DfailIfNoTests=false`
+11. (沿用 P0)`PluginDrivenTransactionManager.begin(ConnectorTransaction)` 暂无
caller — P5/P6/P7 接通
+12. (沿用 P0)`ConnectorMetaInvalidator.invalidatePartition` fallback 到
invalidateTable;`invalidateStatistics` no-op
+13. (沿用 P0)`mvn -pl fe-core test` 不带 `-am` 失败
---
## 📂 当前关键文件清单
-### 本场新增 / 修改(已 commit)
+### 本场(2026-05-25 白天 ④)修改
+
+```
+MOD plan-doc/tasks/P1-scan-node-cleanup.md (元信息 ✅;验收标准重对齐;T1 → 🚫;新增白天 ④ 日志)
+MOD plan-doc/PROGRESS.md (P1 → 100% ✅;P2 → 准备启动;§三 T1 翻
🚫;§四加白天 ④)
+MOD plan-doc/HANDOFF.md (本文件覆盖更新)
+```
+
+工作树状态(本场 commit 前):
+```
+ M plan-doc/tasks/P1-scan-node-cleanup.md
+ M plan-doc/PROGRESS.md
+ M plan-doc/HANDOFF.md
+```
+
+### 待 push 的本地 commit(catalog-spi-02 → upstream-apache/branch-catalog-spi)
+
+```
+43a12a05ffe [refactor](connector) [P1-T03-T05] route plugin-driven scans
first in nereids translator
+??????????? [doc](connector) [P1] close P1 — defer T1 to P8, batch A only
← 本场即将创建
+```
+
+### P2 (trino-connector) 涉及的目标(recon 时确认)
```
-NEW tools/check-connector-imports.sh
(gate script)
-MOD fe/fe-connector/pom.xml
(exec-maven-plugin)
-NEW
fe/fe-core/src/test/java/org/apache/doris/connector/fake/FakeConnectorPlugin.java
-NEW
fe/fe-core/src/test/java/org/apache/doris/connector/fake/FakeConnectorPluginTest.java
(11 tests)
-NEW
fe/fe-core/src/test/java/org/apache/doris/connector/ExternalMetaCacheInvalidatorTest.java
(5 tests)
-NEW
fe/fe-core/src/test/java/org/apache/doris/connector/ddl/CreateTableInfoToConnectorRequestConverterTest.java
(7 tests)
-MOD plan-doc/PROGRESS.md
-MOD plan-doc/tasks/P0-spi-foundation.md
-MOD plan-doc/HANDOFF.md(本文件)
+fe/fe-core/src/main/java/org/apache/doris/datasource/trinoconnector/ (待
recon — 看现状)
+fe/fe-connector/fe-connector-trino-connector/
(已存在;看板里标 70%)
+nereids/glue/translator/PhysicalPlanTranslator.java (T3
fallback 待 P2 完成时清理 trino 分支)
+CatalogFactory.SPI_READY_TYPES (P2 末加
"trino-connector" 进白名单)
```
### 跟踪体系(沿用不变)
```
-plan-doc/ (~225K, 17 文件)
+plan-doc/ (~225K, 18 文件)
├── 00-connector-migration-master-plan.md / 01-spi-extensions-rfc.md
├── README.md / PROGRESS.md / AGENT-PLAYBOOK.md / HANDOFF.md
├── decisions-log.md (18) / deviations-log.md (0) / risks.md (14)
-├── tasks/{_template.md, P0-spi-foundation.md}
+├── tasks/{_template.md, P0-spi-foundation.md, P1-scan-node-cleanup.md}
└── connectors/{_template.md, jdbc, es, trino-connector, hudi, maxcompute,
paimon, iceberg, hive}.md
```
@@ -148,11 +173,10 @@ plan-doc/ (~225K, 17 文件)
## 🧠 给下一个 agent 的 meta 建议
-- **当前分支是 `catalog-spi-00`**。新 session 开场 `git branch --show-current` 确认
-- **批 2(T21-T23, T26-T27)已合入 `catalog-spi-00`**(subject `[feat](connector) add
P0 batch 2 gate + unit tests (T21-T23, T26-T27)`),无需 review 老代码;直接读最新源即可。如果对 6
个新/改文件有调整建议,走 DV 流程登记后再改,不要 silent edit
-- **T24/T25 owner 是用户**,不要自己尝试跑 docker regression-test
-- **Maven build 的 cwd 必须是 `fe/`**,不是 workspace 根;`mvn -pl fe-core` 需要 `-am`;运行
`-Dtest=` 时务必带 `-DfailIfNoTests=false`,否则 upstream 模块(fe-foundation 等)找不到匹配
test 会爆 surefire 错
-- 本场没产生新 decision / deviation——所有 trade-off 在 RFC §15 范围内,由代码注释 + 本 HANDOFF
"开放问题" 列出
-- 本场用 `Mockito.mockStatic` + `Mockito.mock(CreateTableInfo)` 两个套路绕开了重度 fe-core
bootstrap——批 1 的 `CreateTableInfoToConnectorRequestConverter` 同样可以这样测,套路通用。后续
P1/P2 写 unit-test 可以复用
-- **必读 AGENT-PLAYBOOK §六 anti-patterns** 再开始动手
-- **本 HANDOFF 不内嵌 commit hash**——hash 通过 `git log --grep="P0 batch 2"` 或 `git
log --oneline -3` 定位。本场无 amend,HANDOFF 与代码同 commit 落盘
+- **分支 `catalog-spi-02`**:本场结束时含 2 个本地未推 commit(批 A scan-node + P1 关闭
doc)。push 与 PR 创建是**风险动作**,必须先与用户确认(已在本场末尾问过;如本场已 push,下场看 `git log --oneline
-3` 验证 `origin/catalog-spi-02` 同步)
+- **PR 目标分支永远是 `apache/doris:branch-catalog-spi`**(不是 master)
+- **commit message** 沿用 `[refactor|feat|doc](connector) [Pn-Tnn] ...`
前缀风格(AGENT-PLAYBOOK §5.4)
+- **Maven 命令**:cwd=`fe/`;`mvn -pl fe-core -am compile
-Dmaven.build.cache.enabled=false`;测试用 `-Dtest=... -DfailIfNoTests=false`
+- **P2 启动前必读**:`connectors/trino-connector.md`(连接器看板里目前 70% 完成度)+ Master Plan
§3.3 P2 章节
+- **P2 主要工作量预估**:补齐 fe-connector trino-connector 模块剩余 30%(核心是 catalog 注册 +
SPI_READY_TYPES);删 fe-core 侧 trino-connector legacy;清掉 T3 fallback 中的 trino
分支(PhysicalPlanTranslator)
+- **不要试图删 13 个 Jdbc*Client** — P1 阶段已决议推迟到 P8。看到 legacy jdbc client 不要技痒
diff --git a/plan-doc/PROGRESS.md b/plan-doc/PROGRESS.md
index b41dc3f4585..518a1cd8cf2 100644
--- a/plan-doc/PROGRESS.md
+++ b/plan-doc/PROGRESS.md
@@ -1,6 +1,6 @@
# 📊 项目进度仪表盘
-> 最后更新:**2026-05-24(夜 ③)** | 当前阶段:**P0 SPI 缺口补齐**(批 0 + 批 1 + 批 2 代码侧完成;待
T24-T25 用户跑 JDBC/ES regression-test) | 项目总进度:**13%**
+> 最后更新:**2026-05-25** | 当前阶段:**P1 已收口**(in-scope T3+T4+T5 完成;T1 推迟 P8、T2 推迟
P4/P5;待 batch A push + PR)→ **P2 trino-connector 准备启动** | 项目总进度:**20%**
> [README](./README.md) · [Master
> Plan](./00-connector-migration-master-plan.md) · [SPI
> RFC](./01-spi-extensions-rfc.md) · [Decisions](./decisions-log.md) ·
> [Deviations](./deviations-log.md) · [Risks](./risks.md) · [Agent
> Playbook](./AGENT-PLAYBOOK.md) · [Handoff](./HANDOFF.md)
---
@@ -9,9 +9,9 @@
| 阶段 | 范围 | 估时 | 进度 | 状态 | 任务文档 |
|---|---|---|---|---|---|
-| **P0** | SPI 缺口补齐 | 2 周 | ▰▰▰▰▰▰▰▰▰▱ 93% | 🚧 收尾(批 0 + 1 + 2 代码侧完成 T03-T23,
T26-T27;T24-T25 用户在本地跑 regression-test) |
[tasks/P0](./tasks/P0-spi-foundation.md) |
-| P1 | scan-node 收口 + 重复清理 | 1 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | ⏸ 待启动(被 P0 阻塞)| — |
-| P2 | trino-connector 迁移 | 2 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | ⏸ 待启动 | — |
+| **P0** | SPI 缺口补齐 | 2 周 | ▰▰▰▰▰▰▰▰▰▰ 100% | ✅ 完成(PR #63582 squash-merge
`c6f056fa5bd`,T24-T25 流水线全绿)| [tasks/P0](./tasks/P0-spi-foundation.md) |
+| **P1** | scan-node 收口 + 重复清理 | 1 周 | ▰▰▰▰▰▰▰▰▰▰ 100% | ✅ 完成(in-scope
T3+T4+T5 ✅;T1 推迟 P8;T2 推迟 P4/P5;commit `43a12a05ffe` 待 push + PR)|
[tasks/P1](./tasks/P1-scan-node-cleanup.md) |
+| **P2** | trino-connector 迁移 | 2 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | 🚧 准备启动 | — |
| P3 | hudi 迁移 | 2 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | ⏸ 待启动 | — |
| P4 | maxcompute 迁移 | 2 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | ⏸ 待启动 | — |
| P5 | paimon 迁移 | 3 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | ⏸ 待启动 | — |
@@ -19,7 +19,7 @@
| P7 | hive (+HMS) 迁移 | 6 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | ⏸ 待启动 | — |
| P8 | 收尾清理 | 2 周 | ▱▱▱▱▱▱▱▱▱▱ 0% | ⏸ 待启动 | — |
-**全局进度:7%**(25 周计划中处于第 1 周末)
+**全局进度:12%**(25 周计划中 P0+P1 共 3 周完成)
---
@@ -44,7 +44,16 @@
> 状态非 ✅ 的项,按阶段聚合。详细见各阶段 task 文件。
-### P0 — SPI 缺口补齐
+### P1 — scan-node 收口 + 重复清理(✅ 已完成)
+| ID | Task | 批次 | Owner | 状态 | 启动 | 备注 |
+|---|---|---|---|---|---|---|
+| P1-T03 | `PhysicalPlanTranslator.visitPhysicalFileScan` 收口(保留 fallback) | 批
A | @me | ✅ | 2026-05-25 | `PluginDrivenExternalTable` 分支已前置;7 个老分支保留 |
+| P1-T04 | `visitPhysicalHudiScan` 委托给 `PluginDrivenScanNode` | 批 A | @me | ✅
| 2026-05-25 | SPI 分支已加;`incrementalRelation` 待 P3 SPI 扩展 |
+| P1-T05 | `LogicalFileScan.computeOutput` 改走 SPI | 批 A | @me | ✅ | 2026-05-25
| `computePluginDrivenOutput` + `supportPruneNestedColumn` 显式分支 |
+| P1-T01 | 删除 13 个 `Jdbc*Client.java` + `JdbcFieldSchema.java` | 🚫 推迟 P8 | — |
🚫 | — | 2026-05-25 决议(Q4):3 个 fe-core caller 是活的 CDC streaming 代码,删除需 SPI 扩展,P8
收尾时一并做 |
+| P1-T02 | 重复 PaimonPredicateConverter + McStructureHelper 处理 | 🚫 推迟 P4/P5 | —
| 🚫 | — | 用户决议 Q2(2026-05-25) |
+
+### P0 — SPI 缺口补齐(✅ 已完成)
| ID | Task | Owner | 状态 | 启动 | 备注 |
|---|---|---|---|---|---|
| P0-T01 | RFC §16.2 决策点闭环 | @me | ✅ | 2026-05-24 | 全部 18 条决策已敲定 |
@@ -72,8 +81,8 @@
| P0-T21 | `tools/check-connector-imports.sh` 实现 | @me | ✅ | 2026-05-24 | grep
守门;正/负冒烟均通过 |
| P0-T22 | exec-maven-plugin 接入脚本(fe-connector aggregator validate) | @me | ✅
| 2026-05-24 | `inherited=false`;RFC §15.4 等价实现 |
| P0-T23 | `FakeConnectorPlugin` + 11 个 default 行为测试 | @me | ✅ | 2026-05-24 |
覆盖 Connector/Metadata/TableOps/WriteOps/Session/Context 全 default |
-| P0-T24 | JDBC regression-test 全套跑通 | @用户 | ⏳ | — | 用户在本地跑 |
-| P0-T25 | ES regression-test 全套跑通 | @用户 | ⏳ | — | 用户在本地跑 |
+| P0-T24 | JDBC regression-test 全套跑通 | @用户 | ✅ | 2026-05-25 | PR #63582 流水线绿 |
+| P0-T25 | ES regression-test 全套跑通 | @用户 | ✅ | 2026-05-25 | PR #63582 流水线绿 |
| P0-T26 | `ConnectorMetaInvalidator` 路由测试 | @me | ✅ | 2026-05-24 | 5 个
@Test;MockedStatic<Env> |
| P0-T27 | `CreateTableInfoToConnectorRequestConverter` 单元测试 | @me | ✅ |
2026-05-24 | 7 个 @Test;4 partition style + 2 bucket |
@@ -85,6 +94,9 @@
> 倒序,新内容置顶;超过 14 天的条目移除(git log 保留历史)。
+- **2026-05-25(白天 ④)** ✅ **P1 阶段关闭**:批 B (T1) recon 揭示 3 个 fe-core JDBC client
caller(PostgresResourceValidator / StreamingJobUtils /
CdcStreamTableValuedFunction)均为活的 CDC streaming 代码(非 dead code),删除需要在
ConnectorPlugin/ConnectorMetadata 上为 CDC 暴露新 capability(getPrimaryKeys /
getColumnsFromJdbc / listTables)。用户决议(Q4):**推迟 T1 到 P8 收尾**(与 streaming CDC
重构一起做)。P1 in-scope(T3+T4+T5)100% 完成;剩余动作:batch A push + PR
+- **2026-05-25(白天 ③)** ✅ **P1 批 A 完成**(T03+T04+T05 scan-node SPI
收口):`PhysicalPlanTranslator.visitPhysicalFileScan` `PluginDrivenExternalTable`
分支前置(T3);`visitPhysicalHudiScan` 加 SPI 分支并通过 `FileQueryScanNode` setters 透传
`scanParams`/`tableSnapshot`,`incrementalRelation` 记 P3
TODO(T4);`LogicalFileScan.computeOutput` 新增 `computePluginDrivenOutput()`
helper + 显式 `supportPruneNestedColumn → false` 分支(T5)。fe-core BUILD SUCCESS +
checkstyle 0;对当前 SPI 表(JDBC/ES)行为等价;7 个连接器特定分支原地保留作 P3-P7 fallback
+- **2026-05-25** ✅ **P0 全阶段完成**:PR
[#63582](https://github.com/apache/doris/pull/63582) squash-merge 到
`apache/doris:branch-catalog-spi`(hash `c6f056fa5bd`);T24/T25 流水线全绿;P0 阶段进度
100%。新本地分支 `catalog-spi-02` 基于最新 base 创建,**P1 启动**(scan-node 收口 + 重复清理,1 周)
- **2026-05-24(夜 ③)** ✅ **P0 批 2 守门 + 单测完成**(T21-T23, T26-T27;T24-T25 用户跑):新增
`tools/check-connector-imports.sh` grep 守门 + 通过 exec-maven-plugin 在
`fe-connector` aggregator validate 阶段调起(`inherited=false`);新增
`FakeConnectorPlugin`(fe-core test)+ 23 个新 @Test 覆盖 11 个 default 路径 +
ConnectorMetaInvalidator 5 个 routing + Converter 7 个(4 partition style ×
IDENTITY/TRANSFORM/LIST/RANGE + hash/random bucket + 列穿透);39/39 tests
green;checkstyle 0;JDBC/ES regression-test 转交用户在本地执行
- **2026-05-24(夜 ②)** ✅ **P0 批 1 DDL + Partition SPI 完成**(T13-T20):新增
`connector.api.ddl` 包 5 个 POJO(CreateTableRequest + 4 spec);`ConnectorTableOps`
加 4 个 default(createTable(request) +
listPartitionNames/listPartitions/listPartitionValues);`ConnectorPartitionInfo`
追加 rowCount/sizeBytes/lastModifiedMillis;fe-core 新
`CreateTableInfoToConnectorRequestConverter` 覆盖 IDENTITY/TRANSFORM/LIST/RANGE
四种 partition + hash/random bucket;`PluginDrivenExternalCatalog.createTable` 路由到
SPI;fe-core BUIL [...]
- **2026-05-24(深夜)** ✅ **P0 批 0 fe-core
桥接完成**(T09-T12):`ExternalMetaCacheInvalidator` + `ConnectorMvccSnapshotAdapter`
新类、`DefaultConnectorContext.getMetaInvalidator()`
override、`PluginDrivenTransactionManager` 加 SPI `ConnectorTransaction`
重载(legacy auto-commit 不变);fe-core 全编译通过 + checkstyle 0 violations;JDBC/ES 下游
zero-impact
@@ -129,8 +141,8 @@
> 当本项目通过 Claude Code 这类 LLM agent 推进时,跟踪当前 session 状态、handoff 状况和 context 健康度。
-- **本 session 已完成**:P0 批 2 守门 + 单测(T21-T23, T26-T27)—— 1
个新脚本(`tools/check-connector-imports.sh`)+ 1 个 fe-connector aggregator pom 加
exec-maven-plugin + 4 个 fe-core test 新文件(`FakeConnectorPlugin` + 3 个
*Test);39/39 tests green;checkstyle 0;T24/T25 转交用户在本地跑 JDBC/ES regression-test
-- **下一个 session 应做**:等 T24/T25 用户跑完后翻 ✅ → P0 阶段全收尾 → 启动 P1(scan-node
收口);或在等待期间开 P0-T28 benchmark(R-006 缓解,原列入 P1)作为 P0 末加项
+- **本 session 已完成**:P1 批 A (T3+T4+T5) commit `43a12a05ffe`(local,未 push)→ 批 B
(T1) recon 揭示 callers 非 dead code → 用户决议 T1 推迟 P8 → P1 阶段关闭 → 跟踪文档(P1 task /
PROGRESS / HANDOFF)全部同步
+- **下一个 session 应做**:(1)push `catalog-spi-02` 到 morningman fork;(2)`gh pr
create --repo apache/doris --base branch-catalog-spi --head
morningman:catalog-spi-02`;(3)启动 P2 (trino-connector) recon
- **是否需要 handoff**:是,已写新 [HANDOFF.md](./HANDOFF.md)
- **协作规范**:[AGENT-PLAYBOOK.md](./AGENT-PLAYBOOK.md)(context 预算、subagent
使用、handoff 触发条件)
diff --git a/plan-doc/tasks/P1-scan-node-cleanup.md
b/plan-doc/tasks/P1-scan-node-cleanup.md
new file mode 100644
index 00000000000..d2a9521d0ad
--- /dev/null
+++ b/plan-doc/tasks/P1-scan-node-cleanup.md
@@ -0,0 +1,137 @@
+# P1 — scan-node 收口 + 重复清理
+
+> 阶段总览见 [00-master-plan §3.2](../00-connector-migration-master-plan.md)。
+> 协作规范见 [AGENT-PLAYBOOK.md](../AGENT-PLAYBOOK.md)。
+
+---
+
+## 元信息
+
+- **状态**:✅ 完成(in-scope: T3+T4+T5;T1 推迟 P8;T2 推迟 P4/P5)
+- **启动日期**:2026-05-25
+- **目标完成**:2026-06-01(1 周)
+- **实际完成**:2026-05-25(提前;scope 大幅收窄)
+- **阻塞**:无
+- **阻塞下游**:P2 (trino-connector) 可启动;批 A scan-node 收口已就位
+- **主 owner**:@me
+- **分支**:`catalog-spi-02`(基于 `upstream-apache/branch-catalog-spi`;批 A 已 commit
43a12a05ffe,待 push + PR)
+
+---
+
+## 阶段目标
+
+承接 P0 的 SPI baseline,做两件事:
+
+1. **删旧**:清理 fe-core 中已经被 SPI 实现覆盖、但还没删的 legacy 代码(JDBC 旧
client、Paimon/MaxCompute 重复 converter)。
+2. **收口**:把 `PhysicalPlanTranslator.visitPhysicalFileScan` 的 7+ 个 `instanceof
XExternalTable` 分支统一到 `PluginDrivenExternalTable` 路径(迁移期可保留老分支兜底);让
`LogicalFileScan.computeOutput` 通过 SPI 而非 instanceof 拿 metadata 列。
+
+完成后:
+
+- `PhysicalPlanTranslator` 不再 `import` 任何具体 `*ExternalTable` 类(除迁移期 fallback)。
+- 后续每个连接器迁移(P3-P7)只需删掉对应 fallback 分支,不需要触碰 scan-node 主干。
+
+---
+
+## 验收标准
+
+从 master plan §3.2 同步(**两项推迟**已在状态前置标注):
+
+- 🚫 ~~13 个 `datasource/jdbc/client/Jdbc*Client.java` + `JdbcFieldSchema.java`
全部删除~~ — **推迟到 P8**(2026-05-25 决议:3 个 fe-core caller 是活的 CDC streaming 代码,删除需
SPI 扩展,不属 P1 surgical scope。详见任务清单 T1 备注)
+- 🚫 ~~fe-core 重复的 `PaimonPredicateConverter` + `McStructureHelper` 处理完毕~~ —
**推迟到 P4/P5**(用户决议 Q2,2026-05-25)
+- [x] `PhysicalPlanTranslator.visitPhysicalFileScan` 优先走
`PluginDrivenExternalTable` 分支 — 批 A T3
+- [x] `visitPhysicalHudiScan` 通过 `PluginDrivenScanNode` 处理增量场景(分支已就位,P3 Hudi
迁移时激活) — 批 A T4
+- [x] `LogicalFileScan.computeOutput` 不再 `instanceof IcebergExternalTable /
HMSExternalTable` —— **部分达成**:新增 `PluginDrivenExternalTable` 分支前置;Iceberg 分支保留作
P6 fallback —— 批 A T5
+- 🟡 `PhysicalPlanTranslator` 不再 `import` 任何具体 `*ExternalTable` 类(除迁移期
fallback) — **迁移期保留**(用户决议 Q3);7 个连接器特定分支在 P3-P7 各自迁移完成时随主任务删除
+- [x] fe-core 全编译 + checkstyle 0
+- [ ] PR CI 全绿(待批 A push + PR 创建后由 CI 报告)
+
+---
+
+## 任务清单
+
+> ID 永不复用。批次方案 2026-05-25 用户已确认:批 A=T3+T4+T5、批 B=T1、T2 推迟 P4/P5。
+
+| ID | 任务 | 批次 | Owner | 状态 | PR | 启动 | 完成 | 备注 |
+|---|---|---|---|---|---|---|---|---|
+| P1-T01 | 删除 13 个 `Jdbc*Client.java` + `JdbcFieldSchema.java` | **🚫 推迟到 P8**
| — | 🚫 | — | — | — | 2026-05-25 recon 结论:3 个 fe-core
caller(PostgresResourceValidator / StreamingJobUtils /
CdcStreamTableValuedFunction)均为活的 CDC streaming 代码(非 dead code),删除需在
ConnectorPlugin/ConnectorMetadata 上为 CDC 暴露
`getPrimaryKeys`/`getColumnsFromJdbc`/`listTables` 等 capability。用户决议(Q4):不在 P1
阶段做 SPI 扩展,T1 推迟到 P8 收尾,届时与 streaming CDC 重构一起做 |
+| P1-T02 | 重复 `PaimonPredicateConverter` + `McStructureHelper` 处理 | **🚫 推迟到
P4/P5** | — | 🚫 | — | — | — | 2026-05-25 用户决议(Q2):fe-core caller 本身是 P4/P5 要删的
legacy;本阶段不动 |
+| P1-T03 | `PhysicalPlanTranslator.visitPhysicalFileScan` 收口(**保留 fallback**)
| **批 A** | @me | ✅ | TBD | 2026-05-25 | 2026-05-25 |
`PluginDrivenExternalTable` 分支提到 if-else 链最前;7 个老分支原地保留作 P3-P7 迁移期 fallback |
+| P1-T04 | `visitPhysicalHudiScan` 委托给 `PluginDrivenScanNode` | **批 A** | @me
| ✅ | TBD | 2026-05-25 | 2026-05-25 | 新分支前置;`scanParams` + `tableSnapshot` 经
`FileQueryScanNode` setters 透传;`incrementalRelation` 待 P3 Hudi 迁移时 SPI 扩展(TODO
注释已落) |
+| P1-T05 | `LogicalFileScan.computeOutput` 改走 SPI | **批 A** | @me | ✅ | TBD |
2026-05-25 | 2026-05-25 | 新增 `computePluginDrivenOutput()`(与
`computeIcebergOutput` 同 shape,用 `getFullSchema` +
virtualColumns);`supportPruneNestedColumn` 加 `PluginDrivenExternalTable →
false` 显式分支(无新 SPI capability 时保守默认);`IcebergExternalTable` 路径原地保留 |
+
+**状态图例**:⏳ pending / 🚧 in_progress / ✅ done / ❌ blocked / 🚫 deleted
+
+---
+
+## 阶段日志(倒序)
+
+### 2026-05-25(白天 ④)— P1 收尾:T1 推迟到 P8
+
+批 B (T1) 启动前 recon 结论:13 个 legacy JDBC client + JdbcFieldSchema 的 3 个 fe-core
caller **均为活的 CDC streaming 代码**:
+
+- `PostgresResourceValidator.java`(`job/extensions/insert/streaming/`):CREATE
JOB 时校验 PG 复制槽 / 发布,被 `StreamingJobUtils.validateSource` →
`StreamingInsertJob.validateTvfSource` → `CreateJobCommand`/`AlterJobCommand`
链路使用
+- `StreamingJobUtils.java`(`job/util/`):`getJdbcClient()` +
`jdbcClient.getPrimaryKeys()` / `getColumnsFromJdbc()` /
`getTablesNameList()`,CDC 表枚举 + DDL 生成
+- `CdcStreamTableValuedFunction.java`(`tablefunction/`):`cdc_stream` TVF,被
`CdcStream.java:46` 调,streaming 作业执行链路
+
+测试侧:`StreamingJobUtilsTest`
需重写;`JdbcFieldSchemaTest`/`JdbcClickHouseClientTest`/`JdbcClientExceptionTest`
测 legacy 本身(随源删除)。fe-connector 侧 SPI 替换 `Jdbc*ConnectorClient` 已就位,但 **fe-core
不能直接 import**(会破坏 `tools/check-connector-imports.sh` 守门)。
+
+**用户决议(Q4,2026-05-25)**:推迟 T1 到 P8 收尾。理由:
+- 删 T1 需要在 ConnectorPlugin/ConnectorMetadata 上为 CDC use case 暴露新
capability(getPrimaryKeys / getColumnsFromJdbc / listTables),是 SPI 扩展工作,超出
Master Plan §3.2 P1 scope
+- 现状无 runtime 风险——legacy JDBC client 仍在原位,CDC 功能正常
+- P8 收尾阶段与 streaming CDC 重构一起做,避免 P1 阶段引入 1-2 天计划外 SPI 设计工作
+
+**P1 in-scope 完成度**:T3+T4+T5 ✅;T1 推迟 P8;T2 推迟 P4/P5。P1 阶段关闭,准备 batch A push +
PR,进入 P2 (trino-connector)。
+
+### 2026-05-25(白天 ③)— 批 A 编码完成(T3 + T4 + T5)
+
+实施了三处 SPI 收口(保留迁移期 fallback):
+
+- **T3** — `PhysicalPlanTranslator.visitPhysicalFileScan`:把现有 `if (table
instanceof PluginDrivenExternalTable)` 分支提到 if-else 链最前;7
个连接器特定分支(HMS/Iceberg/Paimon/Trino/MaxCompute/LakeSoul/RemoteDoris)原地保留作 P3-P7
迁移期 fallback。
+- **T4** — `PhysicalPlanTranslator.visitPhysicalHudiScan`:在 method 顶部新增
`PluginDrivenExternalTable` 分支,路由到 `PluginDrivenScanNode.create(...)`,通过
`FileQueryScanNode` setters 透传 `tableSnapshot` /
`scanParams`。`hudiScan.getIncrementalRelation()` 增量场景被记为 P3 Hudi SPI 扩展的
TODO(注释已落)。HMS + DLAType.HUDI 路径保留。本分支今日不可达(PhysicalHudiScan 目前只为
HMSExternalTable 创建),P3 Hudi 迁移时激活。
+- **T5** — `LogicalFileScan`:
+ - `computeOutput()`:新增 `PluginDrivenExternalTable` 分支,调新增 helper
`computePluginDrivenOutput()`,用 `getFullSchema() + virtualColumns`(与
`computeIcebergOutput` 同 shape)。JDBC/ES 当前无 hidden cols 也无
virtualColumns,行为等价。Iceberg 分支原地保留。
+ - `supportPruneNestedColumn()`:新增 `PluginDrivenExternalTable → return false`
显式分支。语义无变化(fall-through 也是 false),但显式声明 SPI 默认;未来加 `ConnectorCapability` 时改这里。
+ - 新增 import:`org.apache.doris.datasource.PluginDrivenExternalTable`。
+
+**编译 / Checkstyle**:`mvn -pl fe-core -am compile` BUILD SUCCESS;`mvn -pl
fe-core checkstyle:check` 0 violations。
+
+**测试范围**:三处变更对 JDBC/ES(当前唯一已迁 SPI 连接器)行为等价(fullSchema == baseSchema 且无
virtualColumns;supportPruneNestedColumn 原本就 false)。集成层信号依赖 PR CI 上的 JDBC + ES
regression-test(P0 已基线 PASS)。本地单测层未新增——三处都是路由 reorder + 显式声明,难以在不引入
PluginDrivenExternalTable mock 的前提下意义单测;待 PR review 决定是否补。
+
+### 2026-05-25(白天 ②)— 批次方案确认
+
+用户回复 3 个决策点(HANDOFF Q1/Q2/Q3):
+
+- **Q1 → A → B → C**:先做 T3+T4+T5 scan-node 收口(批 A),再删 legacy JDBC client(批
B),T2 推迟到 P4/P5
+- **Q2 → 推迟 T2**:fe-core PaimonPredicateConverter + McStructureHelper 留到 P4/P5
caller 删除时一并干掉;P1 不动
+- **Q3 → 保留 fallback**:T3 仅把 `PluginDrivenExternalTable` 分支提到最前;老 instanceof
链原地保留,每个连接器在 P3-P7 迁移完成时删对应分支
+
+任务表的"批次"列已同步更新;T2 状态翻 🚫(推迟标记)。
+
+### 2026-05-25(白天)— 阶段启动 + recon
+
+- 新建分支 `catalog-spi-02` 基于 `upstream-apache/branch-catalog-spi`(PR #63582 已合入
`c6f056fa5bd`)
+- Recon 5 个子任务,输出代码侧 facts:
+ - **T1**:13 个 `Jdbc*Client.java`(合计 ~2730 LOC)+ `JdbcFieldSchema.java`(129
LOC)。fe-core 内 3 个外部 caller
必须先解耦:`PostgresResourceValidator.java`、`StreamingJobUtils.java`、`CdcStreamTableValuedFunction.java`。3
个测试需删或迁
+ - **T2**:fe-core 有
`datasource/paimon/source/PaimonPredicateConverter.java`(201 LOC)和
`datasource/maxcompute/McStructureHelper.java`(298 LOC)。fe-connector 侧的对应类是
canonical 版本。fe-core
caller:`PaimonScanNode`、`MaxComputeExternalCatalog`、`MaxComputeMetadataOps`
自身就是 legacy,P4/P5 会删
+ - **T3**:`PhysicalPlanTranslator.visitPhysicalFileScan` lines 726-797(72
LOC),含 8 个 instanceof 分支(HMSExternalTable + 嵌套 DLAType 路由;Iceberg / Paimon /
Trino / MaxCompute / LakeSoul / RemoteDoris /
PluginDrivenExternalTable)。`PluginDrivenScanNode.create(...)` 和
`PluginDrivenExternalTable` 已存在
+ - **T4**:`visitPhysicalHudiScan` lines 821-841(21 LOC),目前断言 HMSExternalTable
+ DLAType.HUDI,构造 HudiScanNode 时传 `getScanParams()` +
`getIncrementalRelation()` 支持增量
+ - **T5**:`LogicalFileScan.computeOutput` lines 201-212(12 LOC),instanceof
IcebergExternalTable 时走 `computeIcebergOutput()` 加 v3 row-lineage
虚拟列。`supportPruneNestedColumn()` 也用了 3 个 instanceof(lines 236-238)
+ - **Bonus**:`nereids/` 目录下还有 ~62 处 `instanceof.*ExternalTable`;P1 范围只覆盖
PhysicalPlanTranslator + LogicalFileScan,其余 50+ 处在 P3-P7 各连接器迁移时随主任务清理
+- 批次方案待用户确认(见 HANDOFF)
+
+---
+
+## 关联
+
+- Master plan 章节:[§3.2 P1 阶段](../00-connector-migration-master-plan.md)
+- RFC 章节:n/a(P1 是 SPI 消费方收口,不涉及 SPI 设计修改)
+- 决策:—
+- 偏差:—
+- 风险:R-008(文档脱节)、R-001(image 兼容回归——T3/T4/T5 收口须不影响序列化路径)
+- 连接器:jdbc(T1)、paimon(T2)、maxcompute(T2);T3-T5 是平台层
+
+---
+
+## 当前阻塞项
+
+无。P1 阶段关闭,剩余动作仅为 batch A push + PR 创建(待用户授权)。下一阶段 P2 (trino-connector) 可启动。
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]