Skip to content

[Draft] Master catalog spi 413 upgrade compat ut - #66213

Open
morningman wants to merge 92 commits into
apache:masterfrom
morningman:master-catalog-spi-413-upgrade-compat-ut
Open

[Draft] Master catalog spi 413 upgrade compat ut#66213
morningman wants to merge 92 commits into
apache:masterfrom
morningman:master-catalog-spi-413-upgrade-compat-ut

Conversation

@morningman

@morningman morningman commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Only for testing #66214

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.62% (1908/2458)
Line Coverage 64.45% (34136/52968)
Region Coverage 64.88% (17566/27076)
Branch Coverage 54.02% (9412/17424)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 33.33% (5/15) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 58.89% (25407/43145)
Line Coverage 42.97% (254501/592313)
Region Coverage 38.63% (201318/521124)
Branch Coverage 39.94% (91532/229170)

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29788 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 790206fc1fed038937cdebc20d783e040f4e1ba8, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17658	4090	4167	4090
q2	1996	322	211	211
q3	10738	1436	813	813
q4	4727	474	336	336
q5	8121	853	585	585
q6	279	167	139	139
q7	813	794	619	619
q8	10465	1553	1631	1553
q9	5822	4370	4342	4342
q10	6847	1760	1479	1479
q11	510	362	323	323
q12	728	577	459	459
q13	18181	3365	2782	2782
q14	285	262	249	249
q15	q16	785	783	714	714
q17	1079	1002	1006	1002
q18	6813	5859	5546	5546
q19	1589	1417	1055	1055
q20	841	813	602	602
q21	6319	2802	2570	2570
q22	447	375	319	319
Total cold run time: 105043 ms
Total hot run time: 29788 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4882	4737	5090	4737
q2	299	325	212	212
q3	4938	5259	4742	4742
q4	2132	2192	1399	1399
q5	4834	4751	4630	4630
q6	237	179	132	132
q7	1930	1796	1515	1515
q8	2213	1932	1923	1923
q9	7183	7131	7086	7086
q10	4644	4587	4123	4123
q11	527	380	352	352
q12	737	731	520	520
q13	2974	3284	2769	2769
q14	280	277	249	249
q15	q16	673	691	613	613
q17	1274	1252	1247	1247
q18	7463	6862	6824	6824
q19	1111	1090	1091	1090
q20	2241	2188	1935	1935
q21	5254	4506	4407	4407
q22	541	467	389	389
Total cold run time: 56367 ms
Total hot run time: 50894 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177369 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 790206fc1fed038937cdebc20d783e040f4e1ba8, data reload: false

query5	4317	629	484	484
query6	457	222	203	203
query7	4869	557	342	342
query8	339	183	172	172
query9	8747	4055	4006	4006
query10	445	352	324	324
query11	5825	2338	2145	2145
query12	156	101	101	101
query13	1250	575	397	397
query14	6225	5247	4880	4880
query14_1	4227	4204	4198	4198
query15	213	203	177	177
query16	1023	461	445	445
query17	1093	706	571	571
query18	2596	476	344	344
query19	209	196	150	150
query20	113	108	109	108
query21	239	163	137	137
query22	13616	13520	13310	13310
query23	17502	16454	16090	16090
query23_1	16167	16201	16156	16156
query24	7512	1743	1267	1267
query24_1	1305	1280	1283	1280
query25	570	459	392	392
query26	1376	355	227	227
query27	2532	565	364	364
query28	4482	1975	1944	1944
query29	1087	635	496	496
query30	347	274	232	232
query31	1122	1129	963	963
query32	109	61	59	59
query33	521	316	244	244
query34	1174	1117	621	621
query35	755	776	682	682
query36	1044	1033	857	857
query37	150	104	88	88
query38	1872	1713	1694	1694
query39	892	879	840	840
query39_1	832	833	846	833
query40	244	166	141	141
query41	64	63	63	63
query42	94	90	88	88
query43	317	319	274	274
query44	1379	765	760	760
query45	194	186	188	186
query46	1128	1218	707	707
query47	2181	2137	2026	2026
query48	426	432	298	298
query49	570	413	303	303
query50	1065	451	357	357
query51	10892	11123	10936	10936
query52	91	86	74	74
query53	254	302	203	203
query54	285	261	222	222
query55	72	77	65	65
query56	294	298	296	296
query57	1342	1301	1203	1203
query58	278	274	254	254
query59	1568	1623	1404	1404
query60	306	279	257	257
query61	155	153	151	151
query62	537	498	435	435
query63	239	207	196	196
query64	2795	1035	866	866
query65	4715	4633	4656	4633
query66	1794	501	390	390
query67	29268	29245	29045	29045
query68	3342	1591	1020	1020
query69	406	314	261	261
query70	908	794	811	794
query71	368	331	333	331
query72	3100	2863	2545	2545
query73	834	840	424	424
query74	5056	4932	4718	4718
query75	2536	2516	2142	2142
query76	2322	1194	755	755
query77	352	395	279	279
query78	12000	11874	11464	11464
query79	1396	1116	765	765
query80	1340	540	467	467
query81	525	330	289	289
query82	626	158	118	118
query83	370	324	291	291
query84	275	158	128	128
query85	973	620	502	502
query86	402	245	227	227
query87	1851	1819	1759	1759
query88	3692	2789	2715	2715
query89	432	371	331	331
query90	1908	198	189	189
query91	203	186	159	159
query92	61	61	56	56
query93	1691	1557	924	924
query94	696	356	338	338
query95	790	597	483	483
query96	1109	784	327	327
query97	2628	2640	2498	2498
query98	215	214	200	200
query99	1081	1105	977	977
Total cold run time: 263600 ms
Total hot run time: 177369 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.55 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 790206fc1fed038937cdebc20d783e040f4e1ba8, data reload: false

query1	0.01	0.01	0.00
query2	0.14	0.08	0.09
query3	0.37	0.25	0.25
query4	1.60	0.26	0.24
query5	0.33	0.32	0.32
query6	1.15	0.67	0.67
query7	0.04	0.01	0.00
query8	0.09	0.07	0.07
query9	0.51	0.38	0.38
query10	0.60	0.62	0.58
query11	0.30	0.17	0.18
query12	0.31	0.19	0.18
query13	0.53	0.54	0.54
query14	0.94	0.93	0.92
query15	0.68	0.60	0.59
query16	0.39	0.38	0.39
query17	1.02	0.98	1.01
query18	0.30	0.29	0.29
query19	1.96	1.83	1.76
query20	0.02	0.01	0.02
query21	15.42	0.38	0.31
query22	4.74	0.14	0.13
query23	15.85	0.49	0.30
query24	2.43	0.60	0.46
query25	0.16	0.11	0.09
query26	0.74	0.28	0.21
query27	0.10	0.10	0.09
query28	3.61	0.87	0.52
query29	12.44	4.22	3.33
query30	0.37	0.30	0.26
query31	2.76	0.60	0.32
query32	3.23	0.59	0.48
query33	3.03	2.97	2.89
query34	15.77	4.07	3.39
query35	3.28	3.29	3.29
query36	0.64	0.52	0.50
query37	0.12	0.09	0.09
query38	0.08	0.06	0.07
query39	0.07	0.06	0.06
query40	0.21	0.18	0.16
query41	0.12	0.10	0.08
query42	0.09	0.06	0.05
query43	0.07	0.07	0.07
Total cold run time: 96.62 s
Total hot run time: 25.55 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 26.32% (5/19) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 75.70% (31855/42082)
Line Coverage 60.32% (354869/588357)
Region Coverage 56.87% (297342/522889)
Branch Coverage 58.31% (133659/229226)

morningman and others added 10 commits July 29, 2026 18:43
This multi-month refactor needs persistent state for progress, decisions,
risks, and cross-session agent handoff. Establishes a file-based tracking
system including dashboard, ADR decision log, deviation log, risk register,
per-stage task files, per-connector tracking, and an agent collaboration
playbook covering context budget / subagent usage / handoff norms. Closes
18 design decisions (D-001..D-018) and registers 14 risks (R-001..R-014).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…T27) (apache#63582)

## Summary

Lands the P0 SPI baseline for the catalog-SPI migration (master plan
§3.1 / RFC §2.1), with zero impact on the already-migrated JDBC + ES
connectors.

- **Batch 0** (commits 1-2): SPI types + fe-core bridges —
`ConnectorMetaInvalidator`, `ConnectorTransaction`,
`ConnectorMvccSnapshot`, `ExternalMetaCacheInvalidator`,
`ConnectorMvccSnapshotAdapter`, `PluginDrivenTransactionManager`
generalization.
- **Batch 1** (commit 3): DDL + Partition SPI —
`ConnectorCreateTableRequest` + 4 spec POJOs, 4 new defaults on
`ConnectorTableOps`, 3 new fields on `ConnectorPartitionInfo`, fe-core
converter, `PluginDrivenExternalCatalog.createTable` routing.
- **Batch 2** (commit 4): Import-gate + unit tests —
`tools/check-connector-imports.sh` wired through exec-maven-plugin;
`FakeConnectorPlugin` covering every default fall-through; routing tests
for the invalidator; converter tests for all 4 partition styles + 2
bucket flavors.

## Commits

- `[feat](connector) add P0 batch 0 SPI baseline: MetaInvalidator /
Transaction / MvccSnapshot` (T03-T08)
- `[feat](connector) wire P0 batch 0 SPI into fe-core` (T09-T12)
- `[feat](connector) add P0 batch 1 SPI: CreateTableRequest +
listPartitions` (T13-T20)
- `[feat](connector) add P0 batch 2 gate + unit tests` (T21-T23,
T26-T27)

## Test plan

- [x] `mvn -pl
fe-connector/fe-connector-api,fe-connector/fe-connector-spi -am compile`
— SPI modules compile
- [x] `mvn -pl fe-core -am compile -Dmaven.build.cache.enabled=false` —
fe-core compile
- [x] `mvn -pl fe-core checkstyle:check` — 0 violations
- [x] `mvn -pl fe-connector validate` — import gate runs and passes
(baseline clean)
- [x] `mvn -pl fe-core -am test
-Dtest='FakeConnectorPluginTest,ExternalMetaCacheInvalidatorTest,CreateTableInfoToConnectorRequestConverterTest,ConnectorPluginManagerTest,ConnectorSessionImplTest'`
— 39/39 green
- [x] `mvn -pl
fe-connector/fe-connector-jdbc,fe-connector/fe-connector-es -am compile`
— downstream connectors compile unchanged
- [ ] JDBC regression-test suite (T24) — to be exercised by this PR's CI
pipeline
- [ ] ES regression-test suite (T25) — to be exercised by this PR's CI
pipeline

## Tracking

Full plan, decisions, and risk log live under `plan-doc/` in the repo
(introduced by 6315983, already on the base branch). Per-task
status: `plan-doc/tasks/P0-spi-foundation.md`.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pache#63641)

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).

- [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 apache#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 <noreply@anthropic.com>
…apache#64096)

### What problem does this PR solve?

Related PR: apache#63582 (P0 — SPI baseline), apache#63641 (P1 — nereids
plugin-driven routing)

Problem Summary:

This is **P2** of the catalog SPI migration and targets the
`branch-catalog-spi` feature branch (continuing P0 apache#63582 and P1
apache#63641). It fully migrates `trino-connector` off the legacy in-tree
`fe-core/datasource/trinoconnector/` implementation and onto the
connector SPI module `fe-connector-trino`, making `trino-connector` the
first connector to complete the SPI consumption playbook that later
connectors will reuse as a template.

All five batches land together so there is no intermediate state where a
newly-created trino catalog cannot be serialized.

**Batch A — complete the SPI surface (`fe-connector-trino` only, no
fe-core changes)**
- `TrinoConnectorProvider.validateProperties`: enforce the required
`trino.connector.name` property at `CREATE CATALOG` time (ported from
the legacy `checkProperties`).
- `TrinoDorisConnector.preCreateValidation`: call `ensureInitialized()`
so plugin loading + connector-factory resolution happen at catalog
creation instead of being deferred to the first `SELECT`.
- `TrinoConnectorDorisMetadata.applyFilter` / `applyProjection`: bridge
Trino native filter/projection pushdown, reusing
`TrinoPredicateConverter` to translate a Doris `ConnectorExpression`
into a Trino `TupleDomain`. `remainingFilter` is conservatively returned
as the original expression to match legacy behavior (conjuncts are not
stripped; BE re-evaluates them).

**Batch B — fe-core bridge for image compatibility**
- `GsonUtils`: atomically replace the three legacy `registerSubtype`
entries (`TrinoConnectorExternalCatalog` / `Database` / `Table`) with
`registerCompatibleSubtype` redirects onto the `PluginDrivenExternal*`
hierarchy. This must be atomic — `RuntimeTypeAdapterFactory` rejects
duplicate labels, so keeping both bindings would throw at static init.
Mirrors what ES/JDBC already did.
- `PluginDrivenExternalCatalog.gsonPostProcess`: extract a
`legacyLogTypeToCatalogType()` helper that maps `Type.TRINO_CONNECTOR` →
`"trino-connector"`; the generic `name().toLowerCase()` would otherwise
produce the wrong `"trino_connector"` (underscore) that `CatalogFactory`
does not recognize.
- `PluginDrivenExternalTable.getEngine()` / `getEngineTableTypeName()`:
add `trino-connector` branches that preserve the legacy engine-name /
table-type display across `SHOW TABLE STATUS` and `information_schema`.

**Batch C — flip the switch**
- Add `"trino-connector"` to `CatalogFactory.SPI_READY_TYPES` so catalog
creation routes through the SPI path.

**Batch D — remove legacy code**
- Drop the `instanceof TrinoConnectorExternalTable` scan branch in
`PhysicalPlanTranslator` (the `PluginDrivenExternalTable` SPI branch
already handles it).
- Drop `case "trino-connector"` in `CatalogFactory`.
- Delete `fe-core/datasource/trinoconnector/` (10 files) and the
now-dead legacy `TrinoConnectorPredicateTest`.
- Route the `TRINO_CONNECTOR` db-build case in `ExternalCatalog` to
`PluginDrivenExternalDatabase` (mirrors the migrated JDBC case).
- **Retained for image compatibility**: the
`InitCatalogLog.Type.TRINO_CONNECTOR` and
`TableType.TRINO_CONNECTOR_EXTERNAL_TABLE` enums, the GsonUtils
redirects, and the `MetastoreProperties` trino-connector entry.

**Batch E — tests + tracking docs**
- 29 JUnit 5 unit tests over the plugin-free converters:
- `TrinoPredicateConverterTest` — `ConnectorExpression` pushdown trees →
Trino `TupleDomain` (EQ / range / NE / IN / IS [NOT] NULL / AND / OR,
Slice encoding), plus graceful degradation to `TupleDomain.all()` on
null/unsupported input.
- `TrinoTypeMappingTest` — Trino SPI type → Doris `ConnectorType`
(scalars, decimal precision/scale, timestamp precision clamp,
array/map/struct, unsupported-type failure).
- `TrinoConnectorProviderTest` — `validateProperties` fast-fails when
`trino.connector.name` is missing/empty.
- No Trino plugin/cluster required; plugin-dependent paths remain
covered by the existing `external_table_p0/p2` `trino_connector`
regression suites.
- Sync the migration tracking docs under `plan-doc/` (already carried on
this feature branch since P0).

**Net effect**: 28 files, +1025 / −2681 (~1656 LOC net removed). Old FE
images holding legacy trino catalogs / databases / tables deserialize
onto the `PluginDrivenExternal*` hierarchy through the GsonUtils
string-name redirect, with engine-name display preserved.

**Deferred (follow-ups, not in this PR)**:
- `trino_connector_migration_compat` regression test (old-image
deserialization) — requires a running cluster + Trino plugin + docker,
unavailable in this dev environment; tracked as a CI/cluster follow-up.
- The plugin-install documentation update lives in the `doris-website`
repo and is handled separately.

### Release note

None

### Check List (For Author)

- Test
- [x] Unit Test — 29 new tests in `fe-connector-trino` (predicate
converter / type mapping / property validation).
- [ ] Regression test — existing `trino_connector` suites cover plugin
paths; the new old-image compat regression is deferred to a CI/cluster
follow-up.
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
        - [ ] Previous test can cover this change.
        - [ ] No code files have been changed.
        - [ ] Other reason

- Behavior changed:
- [x] No. Internal routing moves from the legacy fe-core path to the SPI
path; image compatibility, engine-name display, and pushdown semantics
all mirror the legacy behavior. All batches land together, so there is
no serialization-gap window.

- Does this need documentation?
- [x] Yes. The trino-connector plugin-install doc update is a follow-up
in the `doris-website` repo.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
…tch design (hybrid, T02-T08) (apache#64143)

## Proposed changes

testing with apache#64146

P3 of the catalog-SPI migration (base: `branch-catalog-spi`). Migrates
the **hudi** connector following the **hybrid** strategy (D-019): harden
the dormant HMS-over-SPI hudi connector to correctness parity, build a
test baseline, and write the per-table dispatch design — **all behind
the closed gate** (`SPI_READY_TYPES` unchanged).

> ⚠️ **No user-visible behavior change.** The SPI hudi path stays
dormant (gate closed); hudi queries continue to use the legacy
`HMSExternalTable.dlaType=HUDI` path. This PR removes correctness
blockers ahead of the live cutover (deferred to P7 / batch E).

### What's included

**Correctness fixes (hardening dormant code, behind gate):**
- **T02** — fix hudi JNI `column_types` double bug: emit full Hive type
strings (was Doris bare type names, losing precision/scale/subtypes) and
send `column_names`/`column_types`/`delta_logs` as typed lists
end-to-end (was comma join/split, which shattered `decimal(10,2)` /
`struct<...>`). Matches the BE `hudi_jni_reader.cpp` contract (names `,`
/ types `#` / delta `,`).
- **T04** — fail loud on time-travel / incremental read in the SPI
`visitPhysicalHudiScan` branch (was silently returning the latest
snapshot / silently full-scanning).
- **T05** — real EQ/IN partition pruning in
`HudiConnectorMetadata.applyFilter` (was a placeholder that ignored
predicates and unconditionally switched the partition source from
Hudi-metadata to HMS); faithfully mirrors
`HiveConnectorMetadata.applyFilter`.
- **T07** — column-name casing fix in `avroSchemaToColumns` (top-level
lowercase, mirroring legacy `HMSExternalTable`).

**Test baseline (all three connector modules started P3 with 0 tests):**
- `fe-connector-hudi` (33): type-mapping / schema-parity (COW/MOR
golden) / table-type / partition-pruning / scan-range.
- `fe-connector-hms` (12): shared Hive-type-string parser tests.
- `fe-connector-hive` (14): file-format / partition-pruning (mirrors
T05).
- COW/MOR schema is **type-agnostic** (golden parity vs legacy
`initHudiSchema`); table type only affects scan planning.

**Decisions / design (code-grounded, design-only):**
- **T03** — defer `schema_id`/`history_schema_info` field-id evolution
to batch E (DV-006; not a model-agnostic SPI fix).
- **T06** — keep MVCC/snapshot SPI defaults (opt-out) + document
(DV-007).
- **T08** — `tableFormatType` dispatch design memo + **D-020**: single
`hms` catalog per-table routing via a new backward-compatible
`ConnectorMetadata.getScanPlanProvider(handle)` (per-table provider
seam); refines D-005. The keystone gap is split into M1 (identity
consumption, fe-core reads `tableFormatType` as an opaque string) and M2
(scan routing).

### Deferred to batch E / P7 (not in this PR)
Gate flip (`SPI_READY_TYPES += hms/hudi`), fe-core `tableFormatType`
consumption (M1+M2 implementation), live cutover, delete legacy
`datasource/hudi/`, full incremental/time-travel/MVCC, Iceberg-on-hms
via SPI (needs P6 `IcebergScanPlanProvider`), cluster/runtime
validation.

### Verification
Per task tracking, each code batch landed with: per-module compile +
checkstyle 0 (incl. test sources) + connector import-gate pass + new
unit tests green. The two most recent commits are docs-only
(`plan-doc/`); the code is unchanged since the last green batch. Gate
stays closed → the dormant SPI path is unreachable at runtime → zero
live-path risk. CI re-verifies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…core + make fe-core odps-free (T07-T09) (apache#64300)

Follow-up to apache#64253 (the MaxCompute catalog-SPI cutover). After the
cutover a `max_compute` catalog deserializes to
`PluginDrivenExternalCatalog` and no legacy `MaxComputeExternal*` object
is ever instantiated, so the legacy MaxCompute subsystem in fe-core is
dead code. This removes it and makes fe-core's dependency tree fully
odps-free.

**1. Remove legacy subsystem** (`7a4db351100`)
- Delete 20 fe-core files: `datasource/maxcompute/*` (incl.
`MCTransaction`, `MaxComputeScanNode`/`Split`), the MaxCompute
sink/insert/txn plumbing, and 2 legacy-only tests.
- Clean ~21 reverse-reference sites (imports + dead
`instanceof`/visitor/rule branches), keeping every
`PluginDriven`/connector sibling branch and the image/replay keep-set
(GsonUtils compat strings;
`TableType`/`TransactionType`/`TableFormatType`/`InitCatalogLog.Type`
`MAX_COMPUTE` enums; block-id thrift).
- Rewire 3 tests; e.g. `FrontendServiceImplTest`'s block-id RPC test now
mocks the generic `Transaction` SPI, since `getMaxComputeBlockIdRange`
reads the PluginDriven connector transaction.

**2. Make fe-core odps-free** (`409300a75b8`)
- Drop the two odps deps from `fe-core/pom.xml`.
- Move `MCUtils` from fe-common into
`be-java-extensions/max-compute-connector` (its only consumer after the
removal); keep `MCProperties` (odps-free constants) in fe-common.
- Drop `odps-sdk-core` from fe-common — it was also leaking
netty/protobuf transitively to fe-common's own
`DorisHttpException`/`GsonUtilsBase`, so declare `netty-all` +
`protobuf-java` directly (proper dependency hygiene).

**3. Doc-sync** (`f8c305765e8`) — plan-doc
PROGRESS/HANDOFF/deviations/design tracking notes.

- `mvn -pl :fe-core -am test-compile` (main+test) passes; checkstyle 0
violations; connector import-gate passes.
- `grep -rn com.aliyun.odps fe/fe-core/src` → empty.
- `mvn -pl :fe-core dependency:tree | grep odps` → empty (no odps,
direct or transitive).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he#64446) (apache#64446)

Migrates the legacy in-tree Paimon catalog (fe-core `datasource/paimon`,
`metacache/paimon`, `property/metastore/*Paimon*` and their reverse
references) onto the catalog SPI as a self-contained
`fe-connector-paimon`
plugin, following the maxcompute full-adopter + cutover template.
`paimon`
is added to `SPI_READY_TYPES`, so a Paimon catalog now deserializes to
`PluginDrivenExternalCatalog` and all five functional areas —
normal-table
read, system-table read, DDL, MTMV/MVCC/time-travel (procedures are a
doc-only no-op: fe-core has none) — flow through the generic
`PluginDriven*`
bridge with no Paimon-specific branch in fe-core.

Paimon is the first lakehouse full-adopter to exercise the MVCC /
sys-table /
MTMV SPI surfaces, which become the reusable template for the upcoming
iceberg/hudi migrations, so the cutover is held to byte-parity with the
legacy path. The legacy `datasource/paimon/*` classes are left in place
(now
dead on the cutover path) and removed in a follow-up, mirroring the
maxcompute cutover -> P4 removal.

DefaultConnectorContext)

Source-agnostic seams so any connector can express these capabilities;
every
new method is a default no-op/identity, so hive/iceberg/jdbc/maxcompute/
trino/es are byte-for-byte unaffected.

- MVCC/time-travel: `resolveTimeTravel(session, handle,
ConnectorTimeTravelSpec)`
+ `applySnapshot(...)` (threads a pinned snapshot into the handle before
planScan); new immutable `ConnectorTimeTravelSpec` (SNAPSHOT_ID /
TIMESTAMP /
TAG / BRANCH / INCREMENTAL) that fe-core extracts from SQL;
`ConnectorMvccSnapshot`
  carries `schemaId` for schema-evolution-aware time travel.
- System tables: `listSupportedSysTables` / `getSysTableHandle`; fe-core
composes
  the `{base}${sys}` reference name.
- Scan: proportional split-weight
(`getSelfSplitWeight`/`getTargetSplitSize`),
COUNT(*) pushdown (`getPushDownRowCount`, 7-arg `planScan`), native-read
accounting (`isNativeReadRange`), `getDeleteFiles` (VERBOSE
merge-on-read),
`ignorePartitionPruneShortCircuit` (predicate-driven connectors keep
genuine-null
  partitions).
- Metadata: `ConnectorPartitionInfo.fileCount` +
`SUPPORTS_PARTITION_STATS` (rich
SHOW PARTITIONS); `ConnectorColumn.withTimeZone` (WITH_TIMEZONE DESC
marker,
  independent of the timestamp_tz mapping flag); connector cache control
  (`invalidateTable`/`invalidateAll`, `schemaCacheTtlSecondOverride`).
- `ConnectorContext`: six engine-side hooks the connector must not
implement itself
(`loadHiveConfResources`, `vendStorageCredentials`,
`getBackendStorageProperties`,
`normalizeStorageUri` x2, `getStorageProperties`), bound in
`DefaultConnectorContext`
over fe-core's single-source-of-truth credential/URI/HiveConf utilities
(no
  re-ported logic that could drift).

The metastore-side counterpart of the fe-filesystem StorageProperties
SPI: a
Paimon catalog resolves its backend (hive/dlf/rest/jdbc/filesystem) by
ServiceLoader dispatch on `paimon.catalog.type` instead of a fe-core
switch.

- `fe-connector-metastore-api`: hadoop/SDK-free `MetaStoreProperties`
contracts
  (HMS/DLF/REST/JDBC/FileSystem) carrying only Map/scalar facts.
- `fe-connector-metastore-spi`: `MetaStoreProvider<P>` + first-hit
`MetaStoreProviders`
dispatcher + five impls, registered via META-INF/services.
`HmsMetaStorePropertiesImpl`
reproduces the legacy `buildHmsHiveConf` key set and parity-critical
ordering
(storage overlay before the kerberos block; username/sasl last). Adding
a backend =
  one provider class + one services line (no central enum/switch).
- ServiceLoader is loaded with the SPI interface's own (plugin)
classloader, NOT the
thread-context CL: at CREATE CATALOG the static init first fires on an
FE worker
thread whose TCCL is the FE app loader, so a 1-arg load would cache an
empty
  provider list process-wide.
- fe-core bridges add
`initExecutionAuthenticator(List<StorageProperties>)` so
HDFS-Kerberos doAs is wired on the plugin path (legacy
`initializeCatalog` is dead).

Full read + DDL + partitions + statistics + system tables +
MVCC/time-travel across
all five metastore flavors, planning native (ORC/Parquet) vs JNI reads.

- `PaimonCatalogFactory` (pure flavor switch: Options + Hadoop
Configuration + HiveConf,
classloader-pinned for child-first loading), `PaimonCatalogOps`
(injection seam over
the remote Catalog for offline-testable metadata),
`PaimonConnectorMetadata` (~9 -> 28
methods: schema latest/at-snapshot, sys tables, MVCC/time-travel, DDL,
partitions,
  statistics, table descriptor).
- `PaimonScanPlanProvider` (~6 -> 30 methods): predicate/projection
pushdown,
native-vs-JNI routing, large-file sub-splitting with deletion vectors,
COUNT(*)
collapse, schema-evolution field-id dictionary, `$ro` unwrap to base
FileStoreTable.
- New: `PaimonSchemaBuilder` (CREATE TABLE),
`PaimonIncrementalScanParams` (@incr),
`PaimonTableResolver` (sys/branch-aware handle -> Table),
`PaimonLatestSnapshotCache`
+ `PaimonSchemaAtMemo` (per-catalog caches on the long-lived connector).
- `PaimonTypeMapping` gains the write direction (Doris -> Paimon) for
CREATE TABLE.

- NEW `PluginDrivenMvccExternalTable` (generic MvccTable + MTMV host:
the runtime class
for paimon tables), `PluginDrivenMvccSnapshot`,
`PluginDrivenSysExternalTable` +
`systable/PluginDrivenSysTable`. Behavior is selected by
`ConnectorCapability`
(SUPPORTS_MVCC_SNAPSHOT, SUPPORTS_PARTITION_STATS) — no paimon branch in
fe-core.
- `PluginDrivenScanNode`: MVCC pin threaded at every handle-consumption
site, sys-table
time-travel guard, COUNT pushdown, connector-agnostic EXPLAIN
re-emission.
- Cutover: `CatalogFactory` adds `paimon` to `SPI_READY_TYPES` and drops
the built-in
case; `GsonUtils` removes the 7 built-in `Paimon*` subtype registrations
and adds
replay compat so persisted `PaimonExternalCatalog` (all 5 flavors) /
`...Database`
-> `PluginDriven*` and `PaimonExternalTable` ->
`PluginDrivenMvccExternalTable` on
  deserialization (existing clusters upgrade without losing catalogs).
- Nereids/DDL: `PhysicalPlanTranslator` drops the `PaimonScanNode`
branch;
`UserAuthentication`, `CreateTableInfo`, `ShowPartitionsCommand`
generalized;
`Env.getDdlStmt` renders LOCATION/PROPERTIES for SHOW CREATE (paimon
engine, no
  credential leak).

- fe-filesystem: NEW `HdfsFileSystemProperties` / `HdfsConfigFileLoader`
(typed HDFS;
defaults-laden BE map vs defaults-free FE Hadoop-config map so it cannot
clobber a
co-bound object store's fs.s3a.* during a multi-backend merge); S3 MinIO
support +
  legacy tuning defaults; OSS/OBS/COS ANONYMOUS provider-type; new
`FileSystemFactory.bindAllStorageProperties` /
`FileSystemPluginManager.bindAll`.
- NEW `fe-kerberos` leaf module: dependency-free `AuthType` /
`KerberosAuthSpec` facts
  shared across modules without pulling in Hadoop.
- Packaging: NEW `fe-connector-paimon-hive-shade` relocates
`org.apache.thrift` for
HMS 2.3.7; the paimon plugin-zip excludes fe-thrift/libthrift (provided
parent-first)
to avoid a split `TBase` across loaders; self-contained bundle
(hadoop-aws, AWS-SDK,
hadoop-hdfs-client). `paimon.version` (1.3.1) pinned as the single
property across
FE connector / BE paimon-scanner / preload-extensions for FE<->BE
Table/Split serde.
- BE `PaimonJniScanner.getPredicates()` null-predicate backstop
(no-filter scan; pairs
  with the FE producer now always emitting an empty predicate list).

Parity fixes surfaced by the SPI cutover (each restores legacy
semantics):

- Storage/credentials: canonical `s3.*`/`oss.*` keys ->
`fs.s3a.*`/`fs.oss.*`
(STORAGE-CREDS); canonical `AWS_*` BE creds (STATIC-CREDS-BE); per-table
vended
DLF/STS token (REST-VENDED); `oss/cos/obs/s3a` -> `s3://` for data +
deletion-vector
paths (URI-NORMALIZE); external `hive.conf.resources` hive-site.xml
(HMS-CONFRES);
  real doAs on filesystem/jdbc + wrapped HMS read RPCs (KERBEROS-DOAS).
- Reads/types: paimon columns forced nullable (READ-NOTNULL);
`VARCHAR(65533)` not
widened to STRING (VARCHAR-BOUNDARY); dotted `enable.mapping.varbinary`
/
`.timestamp_tz` keys (MAPPING-FLAG-KEYS); per-type partition-value
rendering
(NATIVE-PARTVAL); genuine `\N` not coerced to NULL
(PARTITION-NULL-SENTINEL);
field-id history dict for rename-safe native reads (SCHEMA-EVOLUTION);
paimon-native
  split encode under `enable_paimon_cpp_reader` (CPP-READER).
- Planning/perf: restore the `force_jni_scanner` escape hatch
(FORCE-JNI-SCANNER);
intra-file native sub-splitting (NATIVE-SUBSPLIT); precomputed COUNT(*)
row count
(COUNT-PUSHDOWN); table row count for CBO / SHOW TABLE STATUS
(TABLE-STATS).
- Time travel / DDL: `FOR TIME AS OF` under CST/PST session zones
(TZ-ALIAS); resolved
+ validated `driver_url` for jdbc catalogs (JDBC-DRIVER-URL); reject a
local-only
  name conflict on CREATE TABLE (CREATE-TABLE-LOCAL-CONFLICT).

plan-doc P5 design / task-list / HANDOFF / deviation tracking notes.

- ~70 new FE unit suites: paimon connector (offline recording fakes:
`RecordingPaimonCatalogOps` / `FakePaimonTable`), fe-core PluginDriven*
MVCC /
sys-table / scan-node / split-weight, metastore-spi dispatch +
per-backend
  properties, fe-filesystem HDFS/MinIO/anonymous, fe-kerberos.
- checkstyle 0 violations; connector import-gate passes; FE main+test
compile clean.
- End-to-end paimon behavior is docker-gated (`enablePaimonTest=true`);
the five
functional areas are covered by the `external_table_p0/paimon`
regression suites,
  green on the latest CI run for this branch.
… make fe-core paimon-SDK-free (T29) (apache#64653)

Follow-up to apache#64446 (the Paimon catalog-SPI cutover). After the cutover
a `paimon`
catalog deserializes to `PluginDrivenExternalCatalog` and its tables to
`PluginDrivenMvccExternalTable`, so no legacy `Paimon*` catalog / table
/ scan-node
object is ever instantiated — the entire legacy Paimon subsystem in
fe-core is dead
code. This PR removes it and makes fe-core's dependency tree fully
paimon-SDK-free
(zero `org.apache.paimon` imports, the 5 paimon maven deps dropped).
Mirrors the P4
maxcompute cutover → legacy-removal (apache#64256).

**1. Remove legacy subsystem + sever reverse-refs** (`7632a074e4b`)
- Delete the dead fe-core paimon files: `datasource/paimon/*` (all 5
flavor catalogs,
`PaimonExternalTable`, `PaimonMetadataOps`, `PaimonUtil`,
`source/PaimonScanNode` /
`PaimonSplit` / `PaimonPredicateConverter` / `PaimonSource` /
`PaimonValueConverter`,
`profile/*`), `datasource/metacache/paimon/*`,
`systable/PaimonSysTable`, and the
  legacy-only tests.
- Sever the live reverse-references to the deleted classes, keeping
every `PluginDriven`
/ connector sibling branch and the image/replay keep-set: drop the
`PAIMON` db-creation
switch arm in `ExternalCatalog`; the paimon engine routing in
`ExternalMetaCacheMgr` /
`ExternalMetaCacheRouteResolver`; the dead `PAIMON_EXTERNAL_TABLE`
branch in
`Env.getDdlStmt` (keep the LIVE `PLUGIN_EXTERNAL_TABLE` SHOW CREATE
LOCATION/PROPERTIES
rendering); the `PaimonSysExternalTable` branch in `UserAuthentication`;
the legacy
clauses + `handleShowPaimonTablePartitions()` in `ShowPartitionsCommand`
(keep
`hasPartitionStatsCapability()` and the
`TableType.PAIMON_EXTERNAL_TABLE` enum).
Landed as one compiling unit (mirrors the P4 apache#64300 precedent:
`PaimonUtils` still
  called the removed `ExternalMetaCacheMgr.paimon()`).
- Inline the `getPaimonCatalogType()` string literals
(`hms`/`filesystem`/`dlf`/`rest`/
`jdbc`) to decouple the still-consumed thin metastore-property classes
from the deleted
  `PaimonExternalCatalog`.

**2. Make fe-core paimon-SDK-free** (`32de7d16702`)
- Strip the dead paimon-SDK catalog-building cluster from
`AbstractPaimonProperties` + the
5 flavors (`initializeCatalog` / `buildCatalogOptions` /
`appendCatalogOptions` /
`getMetastoreType` / `normalizeS3Config` / … — 0 live callers; the
plugin path builds
catalogs connector-side). Keep every LIVE SDK-free duty: `warehouse`
`@ConnectorProperty`,
the Kerberos `executionAuthenticator` doAs wiring (read by
`PluginDrivenExternalCatalog`),
property normalization/validation, `getPaimonCatalogType`,
`Type.PAIMON`.
- Vended credentials: delete `PaimonVendedCredentialsProvider` + its
test + the
`VendedCredentialsFactory` `case PAIMON`. Its SDK methods were dead —
reachable only via
the iceberg-only `getStoragePropertiesMapWithVendedCredentials`; the
real paimon vended
path is the connector's `PaimonScanPlanProvider.extractVendedToken`
(moved off fe-core at
cutover). Relocate its one LIVE duty (the REST "skip static storage map"
gate) to a new
SDK-free `MetastoreProperties.isVendedCredentialsEnabled()` (base=false,
`PaimonRestMetaStoreProperties`=true); `CatalogProperty` routes iceberg
through its
provider (byte-identical) and everything else through the
metastore-props method.
- pom: drop `paimon-core` / `-common` / `-format` / `-s3` / `-jindo`
from `fe-core/pom.xml`.
`fe/pom.xml` `paimon.version` is kept (fe-connector-paimon + BE
paimon-scanner still
  consume it).

**3. Doc-sync** (`58160cbcb56`, `895e214b2bd`) — plan-doc HANDOFF /
PROGRESS / connectors /
P5-T29 legacy-removal design tracking notes.

- `mvn -pl :fe-core -am test-compile` (main + test) passes; checkstyle 0
violations;
  connector import-gate (`tools/check-connector-imports.sh`) passes.
- `grep -rn org.apache.paimon fe/fe-core/src` → empty (main and test).
- `mvn -pl :fe-core dependency:tree -Dincludes=org.apache.paimon` →
empty (no paimon dep,
  direct or transitive).
- End-to-end paimon regression (`external_table_p0/paimon`, docker
`enablePaimonTest=true`)
  passed on this branch.
…kerberos (apache#64655)

**P3b: consolidate the drifted Kerberos/Hadoop authentication
implementations into the new top-level neutral leaf module
`fe-kerberos`** as the single source of truth.

Done as 3 commits:

1. **trino → JDK** (`4a740e1`) — replace the only external dependency in
the auth path, trino's `KerberosTicketUtils`, with a JDK-only
(`javax.security.auth.kerberos`) byte-for-byte equivalent, so the
kerberos path is trino-free.
2. **relocate** (`8898e15`) — move the 13 `fe-common`
`security.authentication.*` classes to `org.apache.doris.kerberos.*` in
`fe-kerberos`; retarget all consumer imports (fe-core + 3
be-java-extensions scanners); merge the duplicate `AuthType`.
3. **unify interface** (`5e3e896`) — merge the two competing
`HadoopAuthenticator` interfaces (fe-common's
`PrivilegedExceptionAction` variant vs fe-filesystem-spi's `IOCallable`
variant) into the single fe-kerberos one, and delete
fe-filesystem-hdfs's own
`KerberosHadoopAuthenticator`/`SimpleHadoopAuthenticator` copies (which
had drifted from the canonical impls). `DFSFileSystem` now routes
through the shared authenticators.

`fe-kerberos` remains a top-level neutral leaf (no dependency cycle).

HDFS filesystem access now uses the same authenticators as the HMS path
(restoring parity). Two intentional behavior changes in
fe-filesystem-hdfs: simple / no-`hadoop.username` now runs as remote
user `hadoop` (was: FE process user, direct); kerberos uses the shared
`LoginContext` + 80%-lifetime refresh.

fe-filesystem-hdfs 79/0/0 (+fe-kerberos/spi), checkstyle 0, connector
import-gate clean, whole-repo grep for the removed symbols = 0.

> ⚠️ docker kerberos e2e (HDFS kerberized + HMS) NOT yet run — the real
gate; UGI login can't be exercised in unit tests.
morningman and others added 23 commits July 29, 2026 18:47
…pache#66004 storage facade)

Also records an upstream-owned regression found while validating: apache#66004 itself breaks
ForwardToMasterTest by double-nesting the /rest/v2/manager/node/* JSON payload
(its parent 0dde273 is green, and so is this branch before the rebase).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ill matches

Issue Number: None

Related PR: None

Problem Summary: e1a8963 ("Support Iceberg Docker stacks on Apple Silicon")
rewrote the CONTAINER_UID assignment in custom_settings.env as
CONTAINER_UID="${CONTAINER_UID:-doris--}" so the value could be overridden from
the environment. Three pipelines patch that file with an exact-literal sed:

    sed -i "s/^CONTAINER_UID=\"doris--\"/CONTAINER_UID=\"doris-external--\"/" ...

(the External Regression pipeline script, regression-test/pipeline/vault_p0/run.sh
and regression-test/pipeline/cloud_p0/run.sh). The rewritten line no longer
matches, so the sed silently became a no-op, the file still expanded to
"doris--", and run-thirdparties-docker.sh refused to start with "Must set
CONTAINER_UID to a unique name in custom_settings.env". External Regression and
vault_p0 aborted before running a single case; cloud_p0 swallows the error but
never starts kafka, so its routine load cases fail for no visible reason.

Keep the assignment literal so the sed keeps matching, and apply the environment
override on the lines around it. The macOS workflow documented in
docker-compose/iceberg/README.md (CONTAINER_UID=dorismac- bash
run-thirdparties-docker.sh ...) keeps working unchanged.

- Test: Manual test
    - CI path: ran both real sed variants (External Regression's and
      vault_p0/cloud_p0's) against the file, then sourced it with no
      CONTAINER_UID in the environment -> "doris-external--", guard not hit
    - macOS path: sourced without sed and with CONTAINER_UID=dorismac- ->
      "dorismac-", override still honored
    - Upstream default: sourced without sed and without the env var ->
      "doris--", still rejected by the guard as before
    - Repeated sourcing is idempotent; sourcing under "set -euo pipefail" is
      safe and leaves no CONTAINER_UID_FROM_ENV behind; bash -n passes
- Behavior changed: No, this restores the pre-e1a8963dad CI behavior while
  keeping the environment override added by that commit
- Does this need documentation: No

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… var

The "Generate incremental licenserc" step died before running any of its
own code:

  An error occurred trying to start process '/usr/bin/bash' ...
  Argument list too long

The changed-file list was passed to the step through the CHANGED_FILES
environment variable. Linux caps a single argv/envp string at
MAX_ARG_STRLEN (32 pages = 128KiB); once a PR touches enough files the
variable exceeds that and execve() of the step's shell fails with E2BIG.
On PR apache#64304 the list is 1713 files / 147771 bytes.

The existing guard only counts files (>= 3000 falls back to the full
config), so a PR can slip under the count and still blow the byte limit
-- apache#64304 changes 2184 files in total. Any PR past roughly 1300 changed
files hits this.

Write the list to a file under RUNNER_TEMP and pass the path instead.
That drops the step output from ~147800 bytes to 185 and removes the
size ceiling entirely; the count guard stays as the "too big, just do a
full check" policy. RUNNER_TEMP rather than the workspace, so the
full-config fallback path does not license-check the scratch file.

The list still never reaches a shell as interpolated text, so the
injection hardening from apache#63486 is preserved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Once the license step can start at all, the check itself reports 12
files. Three groups:

Accidentally committed build/run artifacts, removed: fe/1.tree (334KB of
mvn dependency:tree output) and fe/1.pom, both from the P5 paimon
commit; paimon-scanner/${sys:logPath}, a file created by a log4j config
that never expanded its property, holding a junit run's log; and
.claude/wf-d2-cache-recon.js.

Working documents that stay on the branch, given the standard header:
reset_submodule.sh and the six plan-doc files that .licenserc.yaml's
"**/*.md" rule does not already cover (five .workflow.js and one
.java.txt).

connector-metadata-methods.txt is added to paths-ignore instead.
ConnectorMetadataSurfaceTest.readBaseline() treats every non-blank line
as a recorded method signature -- it skips blank lines but not comments
-- so a header would be read as 16 phantom signatures and fail the test.
This matches how stageUtilTest.txt and net_snmp_normal are already
handled.

Verified: of the 1711 add/modified files, 0 now lack a header. The full
tree scan flags only 126 .avro and 17 extensionless data files, all
pre-existing on master, which the full check on master already passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pendency review

The slim in-repo fe-connector-hms-hive-shade declares
org.codehaus.jackson:jackson-mapper-asl 1.9.2 explicitly, so the dependency
review now sees GHSA-c27h-mcmw-48hv (critical) and GHSA-r6j9-8759-g62w (high)
and fails every PR against this branch.

Nothing new ships. Those codehaus classes already ride inside the prebuilt
org.apache.doris:hive-catalog-shade jar that this module replaces, and Hive's
metastore JSONMessageDeserializer -- which the HMS notification-event path
builds on -- runs on them. Building the shade in-repo only makes the dependency
visible to the scan. Legacy Jackson 1.x is EOL and neither advisory has a fixed
version, so there is no upgrade to take.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o fe-connector-paimon

Upstream apache#65984 adds a relation-scoped `@options('scan.snapshot-id'='1', ...)` clause for
paimon. Its fe-core half is catalog-neutral and merged as-is; its paimon half landed in the
fe-core paimon subsystem this branch deleted (P5-T29), so it moves into the connector.

`@options` is just a sixth time-travel selector, so it rides the existing SPI pipeline rather
than a new one: ConnectorTimeTravelSpec gains Kind.OPTIONS carrying the raw map, paimon's
resolveTimeTravel validates the keys and freezes a mutable selector (scan.mode=latest, a tag,
a wall clock) into an immutable pin, applySnapshot threads it onto the handle, and
resolveScanTable applies it via PaimonScanParams.applyOptions. fe-core never inspects an
option key. BindRelation rejects the clause up front through a new
SUPPORTS_SCAN_PARAM_OPTIONS capability: @options only reaches a connector through the MVCC pin
path, so a table that never enters it would silently answer a historical query with latest data.

Two upstream changes needed no port: initialSelectedPartitions -> NOT_PRUNED and
computeOutput's getFullSchema(scanParams) arm are already covered, because LogicalFileScan
resolves partitions and schema against THIS reference's own version and an explicit pin yields
empty (scan-all) partition maps. Upstream's per-relation resolve-once cache is likewise
provided by StatementContext's version key, which includes the option map.

apache#65984 also widens paimon system tables from "no scan params at all" to a per-view capability
matrix. The generic pipeline for that already exists (resolveSysTableSnapshotPin +
applyMvccSnapshotPin); it only needed a per-system-table question, so
ConnectorScanPlanProvider gains supportsSystemTableIncrementalRead/Options and the
PluginDrivenScanNode guard now names the missing capability instead of refusing wholesale.
paimon's applySnapshot stops returning system handles unchanged so the pin reaches the view.

Three independent hardenings ride along: the @incr null-reset widens from two keys to paimon's
whole inherited read-state family; a $binlog @incr read vetoes COUNT(*) pushdown (its reader
packs an UPDATE_BEFORE/AFTER pair into one logical row); and the JNI-forced system-table set
picks up row_tracking, now sourced from the same capability table.

Deliberately NOT ported, with the reasons recorded in plan-doc/HANDOFF.md so a later rebase
does not re-add them: upstream re-adds jni.enable_file_reader_async to the backend option
list, but no BE/JNI consumer for it exists anywhere, including upstream master itself (apache#65955
deleted it) -- this branch already guards that with a test; and the selector-free @options
preload exemption, which would need the connector's option vocabulary before binding and only
affects warmup latency.

Tests: fe-connector-paimon 421/421, fe-connector-api 110/110, fe-connector-iceberg 1151/1151,
fe-core 8335 tests / 0 failures (2 pre-existing errors: upstream apache#66004's NodeInfo JSON
regression and HFUtilsTest needing network), full FE install + checkstyle green. Upstream's
PaimonScanParamsTest is ported to the connector; its resolution cases now run against real
local paimon tables because no connector module carries mockito. Three mutations verified RED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e-connector-iceberg

Upstream apache#65870 is behaviour-inert: it extracts IcebergScanNode's schema-info
initialization into a @VisibleForTesting helper, documents that an identity
partition column added by a newer spec can still be stored physically in files
written by an older spec, and pins the all-column reader schema with one FE unit
test. This branch deleted the fe-core iceberg subsystem, so the fe-core edit is
moot; what matters is whether the connector upholds the same invariant.

It does, by a different route, so no capability had to be migrated: identity
partition columns are unioned over ALL partition specs into path_partition_keys,
FileQueryScanNode.classifyColumn turns those into TColumnCategory.PARTITION_KEY
(non-file slots), and BE resolves through the field-id dictionary only the slots
it decodes from the file. The upstream test therefore cannot be transplanted
verbatim — it asserts an all-column dictionary the connector deliberately prunes.

Instead pin the property the connector actually relies on, which had no multi-spec
coverage: getIdentityPartitionColumns must union every spec, not just the current
one. Mutating table.specs() to table.spec() turns the new test red and leaves the
other 62 green. Also record the reasoning at the dictionary construction site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he snapshot file

`@options('scan.tag-name'='t')` failed at ANALYZE time on a table whose snapshot had
expired but whose tag survived:

  Snapshot file s3://.../tbl_time_travel_expired_tag/snapshot/snapshot-1 does not exist.
  It might have been expired by other jobs operating on this table.

    SnapshotManager.snapshot(:128)
    PaimonCatalogOps$CatalogBackedPaimonCatalogOps.snapshotSchemaId(:342)
    PaimonConnectorMetadata.resolveTimeTravel(:656)          <- case OPTIONS

A paimon tag owns a RETAINED copy of its snapshot, which outlives the ordinary
snapshot/snapshot-<id> file. The TAG case has always taken both the snapshot id and the
schema id off that copy, which is why `FOR VERSION AS OF 't'` reads an expired-but-tagged
version fine. The OPTIONS case used the tag only to look up the ID and then re-derived the
schema id through snapshotManager().snapshot(id) -- i.e. through the file that is gone.

Take both ids off the retained copy, exactly as the TAG case does. A tag-valued
`scan.version` lands here too, because resolveOptions canonicalizes it into scan.tag-name.
`pinnedSnapshotId`'s tag arm is now unreachable, so it is removed rather than left to
re-hide the bug; its javadoc records why a tag never reaches it.

Not covered by this fix, and untested anywhere today: `@options('scan.snapshot-id'=<expired>)`
dies earlier, inside PaimonScanParams.resolveOptions' tryTravelOrLatest, and surfaces paimon's
raw text instead of the connector's "can't find snapshot by id: N".

Found by External Regression 1007447 (paimon_time_travel.qt_expired_tag_options_count),
which passed on every other PR the same day and failed only here.

Tests: new PaimonConnectorMetadataOptionsTagPinTest drives a real local paimon table with a
real tag and asserts WHICH seam the schema id came from, not just its value -- against the
pristine pre-fix code 2 of its 3 cases go red (schemaId 99 from the snapshot-file seam
instead of 7 from the tag); the third (an explicit snapshot-id pin, which must still resolve
through the snapshot) stays green either way. fe-connector-paimon 424/424.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t at latest

`t$audit_log@options('scan.tag-name'='top_cp0')` failed during ANALYSIS with
"Unknown column 'old_name' in 'table list' in PROJECT clause", while every base-table
@options form on the same table and the same tag bound fine.

A system table is not an MvccTable, and BindRelation returns from handleMetaTable BEFORE
StatementContext.loadSnapshots, so the statement's MVCC map never holds an entry for the
$-suffixed relation. MvccUtil.getSnapshotFromContext therefore answers empty by
construction and LogicalFileScan.computePluginDrivenOutput silently bound the LATEST
schema -- while the scan itself read the pinned snapshot, because the scan half
(resolveSysTableSnapshotPin + applySnapshot's sys arm) does resolve the pin off the source.

The visible symptom is the loud one: a column renamed after the pin cannot bind. The
dangerous one is silent -- a column whose TYPE changed binds at the latest type over
pinned data (in the failing suite `victim` is BIGINT at latest and STRING at the tag).

The port of upstream apache#65984 carried the scan half and the per-system-table capability
matrix, but not upstream's analysis half (PaimonSysExternalTable.getFullSchema(scanParams)
plus LogicalFileScan's arm for it). Its claim that "computeOutput's getFullSchema(scanParams)
arm is already covered" holds for the base table -- which does reach loadSnapshots, which is
exactly why the base-table assertions pass -- and not for the system table.

  - PluginDrivenSysExternalTable resolves the pin off its SOURCE and MEMOIZES it per
    selector. The memo is load-bearing, not an optimization: a MUTABLE selector
    (scan.mode=latest, a wall-clock scan.timestamp-millis) is re-evaluated against the LIVE
    table on every loadSnapshot, so resolving once per consumer would let a commit landing
    between binding and scanning hand them different versions -- re-creating the very skew
    this closes. It is keyed the way StatementContext.versionKeyOf keys, field by field:
    TableScanParams defines neither value equality nor toString(), so an object- or
    toString-keyed memo would miss every lookup and resolve twice.
  - buildColumnHandles gets the same sys fallback as pinMvccSnapshot. Fixing only the
    binding would leave the PROJECTION at latest, so the pinned old-name slot would miss
    the latest-keyed handle map, be silently dropped, and crash BE on the field-id dict --
    the shape of ExtReg 1004351.
  - The paimon connector answers both 3-arg metadata calls for a sys handle from the
    option-applied VIEW (the metadata-side twin of resolveScanTable). Routing them through
    schemaAt would return the BASE table's historical fields and drop the view's own
    columns, e.g. $audit_log's leading `rowkind`.

Scope is wider than the failing query: OPTIONS_SYSTEM_TABLES also covers $ro and $binlog,
which derive their row type from the base table the same way and have no coverage today.

Found by External Regression 1007447
(test_paimon_schema_time_travel_matrix.order_qt_audit_log_options_tag_schema), which passed
on every other PR the same day and failed only here.

Tests: new PluginDrivenSysExternalTableScanPinTest (6) covers source delegation, resolve-once
per selector, independent selectors, no-selector, a declining connector, and a non-MVCC
source; reverting the memo key to object identity turns the resolve-once case red.
PluginDrivenScanNodeSysTablePinTest's two positive cases now assert the delegation and that
loadSnapshot is NOT called again there. fe-core datasource.plugin/scan/mvcc 379/379,
nereids logical+analysis 256/256, fe-connector-paimon 424/424.

Not yet verified end to end: the regression suite itself needs a docker run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e nested-DDL capability

test_iceberg_on_hms_gateway_ddl_parity has never passed, in any environment: it failed on
its first executable assertion in all four builds that ran it (1006034, 1006199, 1006647,
1007447), so lines 89-145 have never executed anywhere.

The suite asserted that nested column DDL SUCCEEDS on an iceberg-on-HMS table, on the stated
premise that the dedicated catalog "succeeds on the very same table". That premise was
carried over from REST-catalog experience and was never checked against a metastore --
every other nested-DDL suite in the repo is REST-backed (iceberg_schema_change and the
partition-evolution suites all use iceberg.catalog.type='rest'). Nothing exercised nested
struct evolution against HMS.

Against HMS it does not hold, and cannot be made to hold from Doris. Iceberg's
HiveTableOperations rewrites the HMS column list from the iceberg schema on every commit, so
adding a nested field changes an EXISTING hive column's type string, which HMS refuses unless
it is primitive widening (hive.metastore.disallow.incompatible.col.type.changes, default true
since Hive 2.0). From UpdateSchema.commit() downward both catalogs run identical iceberg code
against the same metastore, so the refusal cannot differ between them. The FE stack from
1007447 confirms the forwarding fix itself works -- HiveConnectorMetadata.addNestedColumn
-> IcebergConnectorMetadata.addNestedColumn -> IcebergNestedColumnEvolution.addColumn
-> SchemaUpdate.commit -> HiveTableOperations.doCommit -> InvalidOperationException from HMS.
The branch moved the failure from "Doris refuses" to "the metastore refuses".

So assert the relation the suite is named for rather than an absolute capability: run the same
eight-statement sequence through BOTH catalogs, on two identical tables (one each, so the
first run cannot change the schema the second sees), classify each outcome into an
environment-independent verdict, and require the two to agree. This keeps catching the actual
regression -- a gateway that refuses the statement itself instead of forwarding it gets
REFUSED_BY_DORIS while the dedicated catalog does not, so the lists diverge, and a second
assertion rejects REFUSED_BY_DORIS outright. An unrecognized message is filed as OTHER and
fails rather than being silently accepted as parity. The cross-catalog visibility assertions
are kept and run whenever the metastore does permit the change, so an environment that allows
nested struct evolution strengthens the suite rather than changing it.

The comment-parity half (SHOW CREATE TABLE / SHOW TABLE STATUS / information_schema) is
untouched -- it is a pure read path the metastore has no opinion about. Both product fixes
also keep their unit coverage: HiveConnectorMetadataSiblingDelegationTest (16 cases, incl.
everyPerHandleMethodForwardsAForeignHandleToTheSibling) and
HiveConnectorMetadataTableCommentTest (4 cases).

Verified: groovy parse OK; the classifier dry-run maps the real 1007447 HMS message to
REFUSED_BY_METASTORE, the pre-fix gateway message and master's
"Nested add column operation is not supported for this table type." both to REFUSED_BY_DORIS,
and an unknown message to OTHER. Not yet run end to end -- lines 89-145 still need a docker
run to execute for the first time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, not latest

`t$audit_log@options('scan.tag-name'='top_cp0')` bound fine after de87be4 but then failed
at PLAN time with "Column old_name not found in table top_timeline$audit_log"
(FileQueryScanNode.setColumnPositionMapping:315). That commit closed two of the three consumers
of a sys table's pin and missed the third, so the failure moved one stage later rather than away:
External Regression 1007447 failed the same suite line during ANALYSIS ("Unknown column
'old_name' in 'table list' in PROJECT clause"), 1007887 fails it at the scan node.

A system table is not an MvccTable and BindRelation returns from handleMetaTable BEFORE
StatementContext.loadSnapshots, so MvccUtil.getSnapshotFromContext answers empty BY CONSTRUCTION
for a $-suffixed relation. Three places on the scan path need the pin, and each needs its own
fallback off the source table:

  - pinMvccSnapshot -> resolveSysTableSnapshotPin  (already had it)
  - buildColumnHandles                             (de87be4 added it)
  - resolvePinnedFullSchema                        (this commit)

Behind getPinnedFullSchema/getPinnedBaseSchema, resolvePinnedFullSchema still went through the
context lookup, got empty, and fell back to super -- i.e. the LATEST schema -- while the slots
were bound and the column handles built at the PINNED one. setColumnPositionMapping then indexed
the pinned slots into the latest column list.

The loud half is the reported one: `old_name`, renamed to `MixedName` after the tag, has no index
and the query dies. The silent half is worse and is why this is a correctness fix rather than a
message fix -- a column that merely MOVED or changed type maps to the wrong file column and BE
reads wrong data with nothing raised. In the failing suite `victim` is BIGINT at latest and
STRING at the tag.

Fix: route a PluginDrivenSysExternalTable through its own getFullSchemaAt(tableSnapshot,
scanParams) -- the exact method LogicalFileScan.computePluginDrivenOutput binds from -- so
binding, column handles and position mapping share the sys table's memoized pin and cannot
disagree. The instance is the same one for all three: generateTupleDesc carries the
LogicalFileScan's table onto the tuple descriptor, and the scan node's selectors are forwarded
from the same fileScan, so the memo key matches field for field. A reference with no pin (or a
connector that declines the selector) resolves empty and getFullSchemaAt returns getFullSchema(),
which is byte-for-byte what super.getPinnedFullSchema()/getPinnedBaseSchema() returned before --
getBaseSchema(false) is visibleColumns(getFullSchema()) and neither is overridden in the plugin
hierarchy. Both call sites already null-check, so the no-schema path is unchanged.

Tests: PluginDrivenScanNodePinnedSchemaTest gains sysTablePositionMapsAgainstItsOwnPinNotLatest
(the pinned schema reaches getPinnedFullSchema, and the MVCC-context route -- which answers empty
by construction -- is never consulted) and sysTablePinnedBaseSchemaDropsHiddenColumns
(getBaseSchema(false) parity on the pinned schema, since it feeds numOfColumnsFromFile). Removing
the new arm turns both red; restoring it turns both green. fe-core datasource.scan/plugin pin
suites 18/18.

Not yet verified end to end: test_paimon_schema_time_travel_matrix needs a docker run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ually emits

test_iceberg_on_hms_gateway_ddl_parity failed in External Regression 1007887 on the plain-hive
guard at line 195: it expected the refusal to contain "not supported", but got
"Nested column path is only supported for Iceberg tables: s.b".

The product behavior is correct. AlterTableCommand admits nested column paths only for a target
that reports SUPPORTS_NESTED_COLUMN_SCHEMA_CHANGE -- i.e. the gateway routes by TABLE capability,
not by catalog type -- and raises that message otherwise (AlterTableCommand:179, pinned by
AlterTableCommandTest:187). The expectation was master's wording ("Nested add column operation is
not supported for this table type."), a string that appears nowhere in this branch's tree, so the
assertion was vacuous-by-accident here and would have passed on any message containing those two
words.

It surfaced only now because the line had never run. The suite died at its first executable
assertion in all four earlier builds (1006034, 1006199, 1006647, 1007447); 31ae926 reworked
the parity probe into an environment-independent verdict, the probe passed for the first time in
1007887 -- "gateway: [REFUSED_BY_METASTORE], dedicated: [REFUSED_BY_METASTORE]", parity held --
and execution reached line 195 for the first time in the suite's history.

Also teach classifyOutcome the same message. It recognized only "not supported" as
REFUSED_BY_DORIS, and that is precisely the wording a gateway which stopped recognizing its
iceberg tables would NOT produce today -- it would produce "only supported for Iceberg tables"
and be filed as OTHER. Still red, but naming the metastore's absence rather than the regression
the suite exists to catch. Both wordings now classify as REFUSED_BY_DORIS, so the verdict stays
accurate whichever side of the rename the code is on.

Verified: brace/paren/triple-quote balance checked; the two edits are the only behavioral change,
the comment-parity half is untouched. Not yet run end to end -- lines 195+ still need a docker run
to execute for the first time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
run-thirdparties-docker.sh assumes a Linux host in three places, and each
one aborts the run on macOS:

- IP_HOST detection only knows `ip` and `hostname -I`, and macOS has
  neither, so IP_HOST came out empty. Darwin now gets 127.0.0.1, which is
  also the only correct answer there: the "host" the `network_mode: host`
  services join is the Docker Desktop VM rather than the Mac, and Docker
  Desktop forwards the Mac's loopback into it. A LAN address would leave
  the VM and arrive where nothing listens.

- ensure_hosts_alias rewrote /etc/hosts unconditionally, so every run
  needed sudo -- which on macOS has no passwordless default and no tty to
  prompt on. It now builds the file it wants, compares, and escalates only
  when the alias is missing or points somewhere else.

- mysql:5.7.36 publishes a manifest list holding linux/amd64 alone, so an
  Apple Silicon host resolves no manifest and the pull fails. That takes
  hive down with it, because the default JuiceFS metadata URI makes mysql
  an implicit hive dependency. Pinned to linux/amd64 so the host emulates
  the one image that exists.

Separately, the iceberg entrypoint linked /opt/spark/{bin,sbin}/* into
/usr/local/bin with a bare `ln -s`. A container's writable layer survives
a stop, so on `docker start` those links are still present, `ln` fails,
and `set -e` takes the entrypoint down with it -- spark-iceberg could only
ever be recreated, never restarted. Now `ln -sf`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing it

An iceberg table created with `COMMENT '...'` persisted no comment at all.
The COMMENT clause and the PROPERTIES map are two DISTINCT fields on the
create request -- CreateTableInfoToConnectorRequestConverter fills
request.getComment() from the clause and request.getProperties() from
PROPERTIES(...) -- and iceberg's createTable forwarded only the latter, the
same gap legacy IcebergMetadataOps.performCreateTable had. Every reader
downstream (getTableComment, the COMMENT clause of SHOW CREATE TABLE,
information_schema.tables.TABLE_COMMENT, SHOW TABLE STATUS) therefore
reported a table with no comment, through a dedicated iceberg catalog and
through an HMS gateway catalog alike.

Fold the clause into the `comment` table property in createTable. Resolution
mirrors the fix already shipped for paimon (PaimonSchemaBuilder.build): an
explicit properties["comment"] WINS, preserving the legacy persisted-comment
behavior including a deliberately empty one (keyed on containsKey, not on
emptiness), else the COMMENT clause is used. A blank clause writes nothing:
nereids defaults CreateTableInfo.comment to "" when the clause is omitted, so
stamping it unconditionally would add a noise `"comment" = ""` to the
PROPERTIES of every iceberg table Doris creates.

Also fixes two defects this unmasked in test_iceberg_on_hms_gateway_ddl_parity,
whose comment half seeds the comment with a plain COMMENT clause and so had
been asserting against a table that never carried one:

- information_schema is PER-CATALOG: `information_schema.tables` resolves
  inside whichever catalog is current and lists only that catalog's tables, so
  the TABLE_CATALOG predicate can only narrow rows already scoped to the
  current catalog, never reach across to another one. The suite read both
  catalogs from a single session position, so the non-current one silently
  returned zero rows and failed as an NPE. Each read is now taken from inside
  its own catalog.
- Added size() assertions on those reads, so a future regression to zero rows
  reports what actually went wrong instead of an NPE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y cluster and the dead storage/AWS-credential surface (apache#66210)

### What problem does this PR solve?

Issue Number: apache#65185

Problem Summary:

Part of the catalog-SPI migration (apache#65185). Review round 21, targeting
`branch-catalog-spi`.

This round is one self-contained work line: finish the architectural
goal "**`fe-core` holds no
property parsing**" for `org.apache.doris.datasource.property`.
Everything here is deletion of code
that is unreachable on this branch — nothing moves and nothing is
migrated, because every successor
is already live connector-side. `fe-core` loses ~860 lines and gains
none. The investigation, the
per-package verdicts, the rejected alternatives and the rolling handoff
are included under
`plan-doc/fecore-property-cleanup/`.

The two packages under `datasource/property/` were investigated together
but got **different**
verdicts, and are deliberately not treated as one job:

| package | verdict | reason |
| --- | --- | --- |
| `metastore/` (4 files, 333 lines) + the orphaned
`ConnectionProperties` | deleted outright | unreachable at runtime; its
successor has been live for some time — nothing needs to move |
| `common/` (2 files, 237 lines) | **stays in `fe-core`**, only its
provably dead half is cut | it serves *internal* storage (cold-storage
`StoragePolicy`, cloud `StorageVault`), not external datasources; every
relocation target is dependency- or classloader-illegal |

**1. The metastore property cluster is deleted, not migrated**

`datasource/property/metastore/` (4 files) plus the
`ConnectionProperties` base it orphans. The
successor is already running: `fe-connector-metastore-api`
`MetaStoreProperties`,
`fe-connector-metastore-spi` `MetaStoreProviders.bind` and
`Connector.deriveStorageProperties`.

The cluster was unreachable. Its registry held only `TRINO_CONNECTOR`,
whose factory returns a bare
`MetastoreProperties` without ever calling `initNormalizeAndCheckProps`
— so it parsed nothing, and
`createInternal`, `getDerivedStorageProperties`,
`getExecutionAuthenticator` and the
`StorageAuthenticatorBridge` all had zero callers. `CatalogProperty` was
the sole importer and
offered two doors: `checkMetaStoreAndStorageProperties` (no callers
repo-wide) and
`resolveDerivedStorageDefaults`, whose metastore branch runs only when
`pluginDerivedStorageDefaultsSupplier` is null — and
`PluginDrivenExternalCatalog` installs that
supplier unconditionally. Nothing here was Gson-persisted, so no image
or replay compatibility is at
stake (note `MetastoreProperties.Type` is unrelated to the persisted
`InitCatalogLog.Type` /
`InitDatabaseLog.Type` constants of the same name).

`resolveDerivedStorageDefaults` now **throws** when the supplier is
unwired instead of deriving
nothing. That preserves today's behavior, where the metastore parse
threw on this path. Deriving an
empty map would silently drop the iceberg `warehouse` → `fs.defaultFS`
bridge and, because the setter
deliberately does not reset caches, would cache the under-derived
`StorageBindings` for good.
`CatalogPropertyPluginStorageDerivationTest` gains a case pinning that,
and its stale mutation
comments are retargeted at mutations that still exist.

**2. The callerless AWS provider-instance arm is dropped**

`StorageAdapter.getAwsCredentialsProvider()` and its two helpers build a
live AWS SDK v2 credentials
object for FE-side SDK clients;
`AwsCredentialsProviderFactory.createV2`, `createDefaultV2` and the
single-arg `getV2ClassName` existed only to serve that arm. Both
consumers — the catalog connectivity
testers and `IcebergAwsClientCredentialsProperties` — were removed
earlier in this migration along
with the whole `datasource/connectivity` package, so nothing calls them
here anymore.

**This one is a deliberate trade, not merely dead code.** It is still
live on `apache/doris` master,
where both consumers exist; it reads as dead only on this branch.
Removing it means future upstream
edits to that region will surface as modify/delete conflicts on rebase,
to be resolved by keeping the
deletion. The investigation first flipped its own recommendation to *do
not delete* for exactly that
reason (see the `[doc](catalog) record that the dead AWS provider arm is
live upstream` commit); the
owner then accepted the trade, and only after that was the deletion
carried out.

Deliberately **kept**, because they are not part of that arm:
`getAwsCredentialsProviderMode()` and
the `s3CredentialsMode` field, and
`AwsCredentialsProviderFactory.getV2ClassName(mode, boolean)` with
its two env probes. Those emit the provider class-name string that
reaches BE via
`AWS_CREDENTIALS_PROVIDER_TYPE` and the hadoop
`fs.s3a.aws.credentials.provider` map, and
`AzureGuessRoutingParityTest` pins the mode accessor. Folding them into
the look-alike
`fe-filesystem-s3-base` twin was considered and **rejected**: the two
implementations differ in two
live behaviors (the hadoop string gains a `ProfileCredentialsProvider`;
the accepted mode strings
widen), and no test in the repo pins the emitted string — so the swap
would have shipped a regression
green. Three connector comments naming the deleted methods are reworded;
the iceberg twin is now the
only implementation of the provider-instance mapping.

**3. The dead storage doors on the catalog base are removed**

Five callerless members of `ExternalCatalog` — `getHadoopProperties`,
`getConfiguration`, `buildConf`,
`buildHadoopConfiguration`, `ifNotSetFallbackToSimpleAuth` — together
with the `cachedConf` /
`confLock` fields they cached through, and three of `CatalogProperty` —
`getHadoopProperties`,
`getBackendStorageProperties`, `getOrderedStorageAdapters` — with their
two lazily populated fields.
`getConfiguration` was already marked deprecated "until connector SPI
extraction is complete", which
it now is.

The point is not the 135 lines. `initStorageAdapters` is now reachable
only through
`getStorageAdaptersMap` and `getEffectiveRawStorageProperties`, both
called from
`PluginDrivenExternalCatalog`, so "**`fe-core` storage has a single
entrance**" holds by construction
rather than by audit — which is also what backs the fail-loud branch in
item 1.

**4. Two test-infrastructure notes (no code)**

`plan-doc/fe-core-ut-runtime-problem.md` records, with measurements
rather than guesswork, why a full
`fe-core` run takes 3h+: `reuseForks=false` (deliberate, to avoid
singleton conflicts) means a fresh
JVM per class at ~5.5s startup over 1225 classes, `forkCount` comes from
`fe_ut_parallel` which
defaults to 1 unless `FE_UT_PARALLEL` is set, and nereids accounts for
72% of test execution time. It
also records what the FE UT pipeline actually runs — TeamCity
`Doris_Doris_FeUt` invokes the repo's
own `run-fe-ut.sh --coverage`, passes
`-Dmaven.test.failure.ignore=true`, and forces the exit code
back to 0 when a run has more than 6000 tests with at most 10 failures
and 10 errors, so a green FeUt
build is not the same as zero failures. Both notes are directions only,
filed for a separate branch —
raising `FE_UT_PARALLEL` is untested and risks fork contention over
ports, BDB directories and temp
paths.

### Release note

None

### Check List (For Author)

- Test <!-- At least one of them must be included. -->
    - [ ] Regression test
    - [x] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
        - [ ] Previous test can cover this change.
        - [ ] No code files have been changed.
        - [ ] Other reason <!-- Add your reason?  -->

Local verification, stated at the coverage it actually has:

- repo-wide grep for every deleted symbol: clean.
- full reactor **clean** `test-compile` **including test sources** (no
`-Dmaven.test.skip`), after each
of the four deletions: BUILD SUCCESS. Deletion changes cannot be trusted
to incremental compilation,
so `fe-core/target/{classes,test-classes}` was removed before each run.
- `fe-core` `checkstyle:check`: 0 violations.
- targeted suites: 95 `fe-core` tests including the Gson replay suites;
110 `fe-connector-api` tests
with `connector-metadata-methods.txt` unchanged; 52 storage-adapter and
catalog-property tests
  (1 pre-existing `@Disabled` in `LocationPathTest`).
- **the full `fe-core` suite was not run to completion.** It was still
going at 3h29m after 1232 test
classes and was stopped by the owner rather than finished, so the claim
is "no failures among the
  1232 classes executed", **not** "the suite passes". Its one failure,
`ForwardToMasterTest.testAddBeDropBe` (`ClassCastException: JSONObject →
JSONArray`), reproduces
identically on a clean HEAD with these changes stashed, so it is
pre-existing.
- **no e2e was run** (no cluster available here); this PR adds no
regression case. All four deletions
remove unreachable code, and the storage-binding path they sit next to
(iceberg hadoop
`warehouse` → `fs.defaultFS`) is covered by unit tests only. CI is
relied on for the rest.
- no BE file is touched by this PR, so BE was not rebuilt.

- Behavior changed:
    - [x] No.

Every deleted member is callerless on this branch. The one semantic
change —
`resolveDerivedStorageDefaults` throwing when
`pluginDerivedStorageDefaultsSupplier` is unwired — is
not reachable in production wiring, since `PluginDrivenExternalCatalog`
installs that supplier
unconditionally, and it replaces a path that already threw. The one
accepted forward cost is stated in
item 2: modify/delete rebase conflicts against upstream master in the
`StorageAdapter` region.

- Does this need documentation?
    - [x] No.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a manifest-carried API version gate for all four plugin families (apache#66211)

### What problem does this PR solve?

Issue Number: apache#65185

Problem Summary:

Part of the catalog-SPI migration (apache#65185). Review round 22, targeting
`branch-catalog-spi`.

One self-contained work line: give the four plugin families — CONNECTOR,
FILESYSTEM, AUTHENTICATION,
LINEAGE — an API version check that can actually reject a plugin. The
design, the version-change
runbook and the eight-task implementation plan are included under
`plan-doc/designs/`.

**1. The check that existed was true by construction**

`ConnectorProvider.apiVersion()` could never reject anything. The SPI
interface is listed
parent-first, every plugin zip excludes the SPI jar, and no shipped
connector overrides the method —
so the `default` body executed on a plugin's behalf is the kernel's own
code, and the number it
returns is the kernel talking to itself. Raising `CURRENT_API_VERSION`
and the default together
admits every stale plugin; raising only the constant rejects every
plugin, including freshly built
ones. Neither is a check. The other three families had none at all.
`apiVersion()`,
`CURRENT_API_VERSION` and the three comparisons against it are deleted.

**2. One number, physically distributed to both sides of the
comparison**

Each family declares its API version once, as a maven property in its
parent pom (`fe/fe-connector`,
`fe/fe-filesystem`, `fe/fe-authentication`, and `fe/fe-core` for
LINEAGE). That single property flows
to:

- a maven-filtered resource inside the family's SPI module — what the
kernel expects;
- the `<manifestEntries>` of every jar built from that pom — what a
plugin declares.

Same build, same property, so the two can never disagree by accident;
across builds they disagree
exactly when they should. There is no second number, so no test has to
keep them in sync. All four
families start at `1.0`.

Families are derived, not enumerated: the token `connector` implies the
kernel resource
`/META-INF/doris/connector-plugin-api-version.properties` and the
manifest attribute
`Doris-Connector-Plugin-Api-Version`. A fifth family follows the
convention on both sides and needs
no new code in the gate.

**3. The gate**

`ApiVersionGate` (new, in `fe-extension-loader`, family-neutral)
compares **MAJOR only**. Minor and
patch are ignored in both directions, which is sound only because
"major" is defined as **any** change
to the SPI surface, additions included — a compatible minor can never
change the set of API elements a
plugin can reach. A jar that declares nothing is refused: fail-closed is
what makes the check
meaningful for plugins built before this existed.

`DirectoryPluginRuntimeManager#loadAll` takes the gate as a
**mandatory** fifth parameter, not an
optional one — an unwired gate would be the same silently-true check
this PR removes. It runs after
`loadClass` but before `asSubclass`/`newInstance`, so an incompatible
plugin's constructor never
executes and the operator gets a message naming both versions instead of
a `ClassCastException`.
Directory loading is the only production path and classpath
`ServiceLoader` builtins never pass
through it, so they are exempt structurally rather than by a flag.

All four families are wired. LINEAGE loads at start-up, and one refused
plugin still cannot stop FE.
AUTHENTICATION loads lazily and reports "no factory for this type" from
a different place than the
load, so the rejection reason is carried into that exception at both
call sites — otherwise a refused
plugin is indistinguishable from one that was never installed.

**4. Four SPI surface baselines**

One per family, freezing what that family's plugins implement or call —
including `fe-extension-spi`'s
`Plugin` / `PluginFactory` / `PluginContext` in all four, so a change
there turns all four red at
once. Signatures are recorded **with their return type**: a changed
return type is a major change by
the same definition, and the older name-and-parameters-only baseline
(`connector-metadata-methods.txt`)
cannot see it. No test can prove someone actually bumped the property —
a test sees the current state,
never the delta — so the failure message says, in the same commit as the
diff, that this is a major
change. The four baselines cannot carry a license header (every
non-blank line is read back as a
signature), so they join `connector-metadata-methods.txt` in
`.licenserc.yaml`.

**5. Two build facts, both found by trying rather than by reasoning**

- **maven-build-cache hashes a module from its sources, dependencies and
`<build><plugins>`, NOT from
`<properties>`** — and the filtered resource's source text is the
literal placeholder. AUTHENTICATION
and LINEAGE were going to ship with the property alone, since they have
no in-tree plugin zips, which
would have put their version outside every hashed input. Bumping 1.0 →
2.0 then produced an identical
module checksum (`4c1d34a6e04836c6` both times) and a cached jar still
declaring `api.version=1.0`.
All four families now carry the `<manifestEntries>` block, and the
checksum tracks the bump
(`a388a287b5a7e172` → `cfb1d83e91b92b98`). Worth recording how this was
caught: adversarial review
out-voted it 2:1 as "not a defect" on the reasoning that a major bump
always comes with an SPI surface
change — the one reviewer who actually ran the A/B build was the one who
was right.
- `fe-connector-es` and `fe-connector-trino` never excluded
`fe-filesystem-api` from their plugin zips,
shipping a duplicate that parent-first loading guarantees is never read.
Now excluded, so all eight
  connector zips agree.

Plan docs are updated to match: D-009 ("the API version never bumps
within this plan") is marked
superseded, and the master plan's §2.3 rule is replaced — the bump
criterion is now "any SPI surface
change, additions included", not "signature change or method removal".

### Release note

None

### Check List (For Author)

- Test <!-- At least one of them must be included. -->
    - [ ] Regression test
    - [x] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
        - [ ] Previous test can cover this change.
        - [ ] No code files have been changed.
        - [ ] Other reason <!-- Add your reason?  -->

Local verification, stated at the coverage it actually has:

- full reactor `package` with the **build cache disabled** and test
sources compiled
(`-Dmaven.build.cache.enabled=false`, no `-Dmaven.test.skip`): BUILD
SUCCESS. `test-compile` is not
sufficient here — `fe-connector-hms` compiles against a shade artifact
that binds to `package` — and
  the cache must be off for the reason in item 5.
- `checkstyle:check` on all six touched modules: 0 violations.
- tests: `fe-extension-loader` 19 (the gate's decision rule, and the
loader's behaviour on real plugin
jars assembled by the test with and without the attribute); `fe-core` 56
targeted (each family's
  wiring); authentication handler 20.
- the produced artifacts were read back rather than assumed:
`doris-fe-connector-es.jar` carries
`Doris-Connector-Plugin-Api-Version: 1.0` alongside the inherited
`Implementation-*` entries, and
`doris-fe-connector-spi.jar` carries `api.version=1.0` rather than an
unfiltered placeholder.
- **no e2e was run** (no cluster available here) and this PR adds no
regression case. The behaviour that
would want one — an operator's pre-existing plugin zip being refused —
is covered by the unit tests
  that build real jars, not by a deployment test.
- no BE file is touched by this PR, so BE was not rebuilt.

- Behavior changed:
    - [ ] No.
    - [x] Yes. <!-- Explain the behavior change -->

A directory-loaded plugin jar that declares no API version, or one whose
major differs from this FE's,
is now refused at load time; previously nothing was ever refused.
Fail-closed is deliberate — accepting
an undeclared version would leave the check meaningless for exactly the
plugins built before it existed
— and rebuilding the plugin against this tree is the fix. In-tree
plugins need no change: they inherit
the property from their family's parent pom, so the number appears in
exactly one place. Classpath
`ServiceLoader` builtins are unaffected, since they do not go through
directory loading.

- Does this need documentation?
    - [ ] No.
- [x] Yes. <!-- Add document PR link here. eg:
apache/doris-website#1214 -->

Not on the website yet, and not in this PR. Third-party plugin authors
now have to stamp one manifest
attribute, so this becomes user-facing documentation at the point where
external plugins are documented
at all — which this branch has not reached. Until then the runbook lives
with the design:
`plan-doc/designs/2026-07-29-plugin-api-version-check-design.md` covers
how to tell major from minor,
the Doris-side steps for each, what a third-party author must write, and
how to diagnose a plugin that
failed to load.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rcement onto the connector SPI

Upstream apache#66112 (8305fe7) makes an iceberg `MERGE INTO` reject a target row that more
than one source row matches, independently of `enable_strict_consistency_dml`. It did that
in fe-core classes this branch no longer has (`LogicalIcebergMergeSink`,
`PhysicalIcebergMergeSink`, `planner.IcebergMergeSink`, `IcebergMergeCommand`,
`IcebergUpdateCommand`), so the rebase brought in the BE half and the thrift field
(`TIcebergMergeSink.require_merge_cardinality_check`) with no FE producer: BE resolved the
unset field to false and its duplicate-match validation was dead code.

`UPDATE` and `MERGE INTO` synthesize the same sink here, so the statement kind is carried
the way upstream carries it - one boolean stamped at the plan builder:

  ExternalRowLevel{Merge,Update}PlanBuilder (true / false)
    -> Logical/PhysicalExternalRowLevelMergeSink
    -> RequestPropertyDeriver  (MERGE keeps the merge distribution even with
                                enable_strict_consistency_dml off - BE can only see the
                                duplicates when both matches reach one instance)
    -> PhysicalPlanTranslator -> PluginDrivenTableSink
    -> ConnectorWriteHandle#isRequireMergeCardinalityCheck (new default-false SPI method)
    -> IcebergWritePlanProvider stamps TIcebergMergeSink.

The flag is kept off the existing `WriteOperation` axis on purpose: it is a statement-level
SQL requirement, not a sink dialect, and the engine also reads it for the distribution
decision. Every other connector keeps the default false, so no non-iceberg write changes.

Also updates PhysicalExternalRowLevelMergeSinkTest for the behaviour apache#66112 changed in a
hunk that merged cleanly: with `enable_iceberg_merge_partitioning` off the sink now asks for
a DistributionSpecMerge that routes only the DELETE images by row id, instead of hashing
every row by a row id that is NULL on the insert side (which serialized all inserts onto one
exchange channel). The test still encoded the old DistributionSpecHash and was failing.

Tests: unit coverage for each hop - origin (both polarities), deriver, translator threading,
and the thrift stamp; 4-way mutation check confirms each hop's test goes red when its link
is broken. FE 173 tests green in the touched classes; checkstyle 0 violations.
E2E: regression-test/.../iceberg/write/test_iceberg_write_merge_duplicate_source_negative
(un-gated by apache#66112) is the end-to-end gate and needs a docker run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he refresh onto the plugin bridge

Upstream apache#66112 (8305fe7) fixed three CREATE TABLE defects in fe-core's legacy paimon
subsystem, which this branch has migrated into the generic plugin bridge. The upstream code
merged cleanly but is inert here, so each is re-implemented where it now belongs. All three
are connector-agnostic on this branch, not paimon-only.

1. Reject an unsupported CTAS destination BEFORE publishing metadata.
   Upstream moved the sink construction ahead of `Env.createTable`; that hunk is on this
   branch, but `UnboundTableSinkCreator` admits ANY PluginDrivenExternalCatalog, so a CTAS
   into a connector with no write path (paimon / es / hudi / trino) still created the remote
   table and only failed later in PhysicalPlanTranslator. The fallback then dropped the
   table BY NAME - which cannot tell this statement's table from a concurrent creator's
   table of the same name, i.e. it can drop someone else's. The CTAS overload of
   createUnboundTableSink now asks the connector-level write provider first. A gateway
   connector answers it (hive/iceberg/jdbc/maxcompute return a provider), so only the
   genuinely write-less connectors are refused, with the same wording the translator used.

2. Refresh the FE table-name cache on an IF NOT EXISTS no-op.
   `PluginDrivenExternalCatalog#createTable` returned true BEFORE the post-create cache
   invalidation, so a table created out-of-band (another engine / another FE) stayed
   invisible to SHOW TABLES and to the following SELECT. A neighbouring probe happened to
   self-heal it, which made the guarantee accidental; it is now explicit.

3. Report a concurrent creator instead of swallowing it.
   The existence probe and the remote create are not atomic. The paimon connector forwarded
   `isIfNotExists()` as paimon's `ignoreIfExists`, so a creator that won the race in between
   was silently no-op'd and this statement looked like the creator - a CTAS would then INSERT
   into, and on failure roll back, the winner's table. Paimon now always creates with
   `ignoreIfExists=false` (as upstream's fe-core arm now does) and the bridge turns the
   resulting failure back into an IF NOT EXISTS no-op returning true, after re-probing that
   the table really is there. Without IF NOT EXISTS the failure still surfaces.

Tests: bridge tests for both no-op arms and both race polarities, a new
UnboundTableSinkCreatorTest for the CTAS admission gate, and the paimon DDL tests updated to
the new contract (they encoded the pre-apache#66112 passthrough). Mutation check: removing the
cache refresh or the admission gate turns the respective tests red. 76 tests green in the
touched classes; checkstyle 0 violations.
E2E: regression-test/.../paimon/test_paimon_ctas_atomicity_negative (un-gated by apache#66112)
covers all three and needs a docker run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pache#66112)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ctor SPI

Upstream 794d514 (apache#65991) added `INSERT [OVERWRITE] ... PARTITION(col='val')`
for hive. All five files it touched are legacy fe-core hive code that this branch
deleted, so the rebase dropped everything except the InsertUtils hunk. The e2e
suite it added, test_hive_write_static_partition.groovy, survived byte-identical
and cannot pass without this port.

Worse, the generic sink made the gap SILENT. UnboundTableSinkCreator already
passes staticPartitionKeyValues for every PluginDrivenExternalCatalog, so hive
reaches bindConnectorTableSink with a static spec that upstream's legacy path
never received. BindSink then excludes the partition column from the bound
columns (so the arity check passes) and NULL-fills it -- and the hive BE derives
the partition DIRECTORY from that row value, mapping NULL to the literal
__HIVE_DEFAULT_PARTITION__ (be/src/exec/sink/writer/vhive_table_writer.cpp:441).
Upstream errored out here; this branch wrote to the wrong partition, silently.

Four changes, one per owner:

1. HiveWritePlanProvider now declares requiresMaterializeStaticPartitionValues().
   This is THE data fix: it makes BindSink re-project the PARTITION literal into
   the row. Unlike iceberg, hive's files do not retain the partition column, but
   the BE still reads it from the row to build the directory, and THiveTableSink
   has no static-partition field to carry it out of band -- the row is the only
   channel. The SPI javadoc said "data files physically retain partition
   columns"; broadened to name both reasons a connector lands here.

2. HiveConnectorMetadata.validateStaticPartitionColumns stops being a no-op and
   rejects an unknown partition column / an unpartitioned table, case-insensitively
   (HMS lowercases hive names; Locale.ROOT keeps the fold locale-independent).
   The knowledge and the messages stay in the connector per the SPI contract.
   The wording is upstream's hive wording, deliberately NOT iceberg's -- iceberg
   says "is not partitioned", hive says "is not a partitioned table", and the e2e
   substring-matches the latter.

3. BindSink resolves static-partition names through table.getColumn() before
   using them, and rejects a column named twice or named in BOTH the PARTITION
   clause and the insert column list. getColumn() is already equalsIgnoreCase and
   is what the two sibling statements in bindConnectorTableSink use; only the
   exclusion filter compared raw names, so this removes an inconsistency rather
   than putting a case rule in the engine. Safe because SchemaCacheValue rejects
   any schema with two columns differing only by case. A name matching no column
   passes through verbatim -- on iceberg the PARTITION clause names a partition
   FIELD (category_bucket), which the connector already validated.
   The duplicate check matters because columnToOutput is a CASE_INSENSITIVE_ORDER
   map: PARTITION(dt='1', DT='2') would otherwise silently keep only one value.

4. MaxComputeConnectorMetadata gains a fail-loud static-partition validator.
   Pure regression guard: maxcompute consumes the spec by EXACT key lookup on the
   raw user-typed keys, which (3) does not reach, so a case-mismatched
   PARTITION(DS='x') would have flipped from today's loud arity error to a
   silently empty PartitionSpec. Matching stays case-SENSITIVE -- ODPS's own case
   semantics are not verified here, so this preserves fail-loud rather than
   speculatively making maxcompute accept the name.

NOT ported: upstream's fe-core validateHiveStaticPartition (its knowledge moves
into the connector, above) and its blanket toLowerCase fold (replaced by schema
resolution, which cannot mangle iceberg partition-field names). Upstream's
HiveDDLAndDMLPlanTest additions are not re-homed: this branch deleted that class
with the hive migration, fe-core does not depend on fe-connector-hive, and no
TestWithFeService harness here wires a plugin-driven catalog; the assertions are
covered by the unit tests below instead.

Verification:
- full reactor `package` (build-cache disabled, checkstyle active): BUILD SUCCESS
- 1409 unit tests green: fe-core 119, hive+iceberg 1158, maxcompute 132
- 7 mutations each turn the intended test class RED and no other:
  materialize->false, hive validator case-sensitive, hive validator no-op,
  maxcompute validator case-insensitive, drop canonicalization, drop the
  duplicate check, drop the insert-column-list reject
- e2e test_hive_write_static_partition.groovy needs a hive docker env (owner-run)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQapWkzRN75GAxeQ3Xaw6B
…pache#65991)

Captures the one trap worth carrying forward: unifying the per-connector unbound
sinks turned upstream's hard error into a silent wrong-partition write for hive,
because the shared sink hands every plugin-driven catalog a static-partition spec
that the legacy hive path never received. Git reported no conflict there and the
tests were green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQapWkzRN75GAxeQ3Xaw6B
…ing replay

Replaying OP_CREATE_CATALOG builds the connector synchronously, on the thread
that is starting the FE, and EditLog's fallback turns anything thrown there
into System.exit(-1). So a connector that rejects a property an older FE
stored without validating -- or a half-installed plugin throwing
NoClassDefFoundError -- keeps the whole FE from starting instead of making one
catalog unusable. The image path never had this problem because it builds the
connector lazily.

Catch on the replay arm only and fall through to the degraded registration
that already exists for an unclaimed type; the failure resurfaces at first
access as an ordinary query error. Interactive CREATE CATALOG still fails
loud, because the user is waiting for the error.

Catching RuntimeException | Error rather than Exception is deliberate: a
half-installed plugin throws NoClassDefFoundError, not a RuntimeException.

Covered by Legacy413JournalReplayTest#connectorConstructorThatThrowsMustNotTakeTheFeDown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mework

Adds golden FE metadata fixtures produced by real Doris 4.1.3 bytecode, and
the tests that replay them on this branch. Goal: a catalog created on 4.1.3
must migrate onto the connector SPI and remain usable, through both the image
and the edit-log path.

The fixtures are generated, never hand-written. 4.1.3's wire format has at
least five details a hand-authored blob gets wrong: HTML escaping is on
(jdbc_url persists '=' and '&' escaped), serializeNulls is off (the resource
key vanishes when null), taap is polymorphic ({} when empty, an array of
2-tuples when not), clazz is written first, and the create path rewrites the
property map before it is persisted. The generator, its provenance and the
regeneration recipe live next to the fixtures; output is byte-deterministic.

Fixture families, and why the resource-backed one is not optional: with a
"type" property present, deleting the migration entirely leaves every
assertion green, because getType() just reads what was already on the wire.
Only the resource-backed catalogs -- which persist no "type" and recover it
from logType -- can fail when the migration is removed. Mutation-verified.

Tests:
  Legacy413ImageMigrationTest       all 20 legacy catalog labels migrate;
                                    engine-owned types (doris) do not; type
                                    recovery incl. the trino-connector hyphen;
                                    comment/lastUpdateTime/taap/jdbc_url fidelity
  Legacy413LabelRegistryTest        every label 4.1.3 could write still resolves,
                                    compared registry-to-registry rather than
                                    against a hand-maintained list
  Legacy413JournalReplayTest        edit-log replay, property fidelity through to
                                    the connector, degraded paths
  Legacy413CloudImageTest           cloud mode; one guard, not a second matrix,
                                    because the cloud and non-cloud modules differ
                                    in exactly one entry
  Legacy413ProviderTypeContractTest the recovered type must be a string a REAL
                                    shipped provider answers to -- the two halves
                                    are literals in different modules

fe-core gains test-scope-only dependencies on the connector modules, matching
the existing fe-filesystem-* arrangement, solely so that last test can compare
both halves. Production still loads connectors from plugins/connector/.

Anti-vacuity: five mutations were applied and each turned the predicted test
red -- disabling the type backfill, dropping the TRINO_CONNECTOR case,
deleting a GSON label, reverting the replay guard, and removing a connector
from the shipped set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@morningman
morningman force-pushed the master-catalog-spi-413-upgrade-compat-ut branch from 790206f to 95b7435 Compare July 29, 2026 13:22
@morningman

Copy link
Copy Markdown
Contributor Author

run feut

…vn test` can compile against them

fe-connector-hms-hive-shade and fe-connector-paimon-hive-shade carry no
sources: their Hive metastore content exists only once maven-shade has run.
With both maven-jar-plugin and maven-shade-plugin bound to `package`, any
build that stops earlier leaves those modules with nothing but an (empty)
target/classes, and that is what the reactor then hands to their consumers.

run-fe-ut.sh builds with `mvn test`, which stops before `package`. Once
fe-core took test-scope dependencies on the connector impl modules, the FE UT
reactor grew from 40 to 59 modules and began pulling fe-connector-hms in,
where every org.apache.hadoop.hive.* import failed to resolve (TeamCity FE UT
1009434: "package org.apache.hadoop.hive.metastore.api does not exist", build
aborted before fe-core ran a single test). fe-connector-paimon would have
failed the same way on HiveConf one module later.

Move both executions to process-classes -- the last phase still ahead of
test-compile -- keeping maven-jar-plugin declared first so the main artifact
is present before shade, which is what the package-phase pin was protecting
under a parallel reactor. Cost is ~21s per FE UT run (paimon 17s, hms 4s),
since the build cache is disabled at project level for both modules.

Verified: the full 59-module `mvn -pl fe-common,fe-core -am test-compile`
succeeds and reproducibly fails on fe-connector-hms without this change; both
shade jars rebuild identical to the previous ones (same byte size, same 7940
/ 55313 entry listings); `package` still runs each shade exactly once and the
hive / paimon / iceberg / hudi plugin zips each carry their shade jar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JZp6xJNzwavabuea7bsAP
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

…-classes)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JZp6xJNzwavabuea7bsAP
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.63% (1909/2459)
Line Coverage 64.50% (34171/52976)
Region Coverage 64.90% (17577/27082)
Branch Coverage 54.15% (9437/17428)

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.

2 participants