Skip to content

[refactor](catalog) decouple external catalogs from FE core into loadable connector plugins - #64304

Merged
morningman merged 92 commits into
masterfrom
branch-catalog-spi
Jul 30, 2026
Merged

[refactor](catalog) decouple external catalogs from FE core into loadable connector plugins#64304
morningman merged 92 commits into
masterfrom
branch-catalog-spi

Conversation

@morningman

@morningman morningman commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #65185

Related PR: every phase PR is listed in #65185

Problem Summary:

This is the merge of the branch-catalog-spi feature branch, i.e. the Catalog SPI
migration tracked in #65185. Per-phase design, review history and the per-connector
status table live in that issue. The framework itself is documented in
fe/fe-connector/README.md (architecture, module map, how to add a connector) and
fe/fe-connector/AGENTS.md (build/test recipes, architecture gates, invariants).

Before: fe-core hard-coded every external data source under
fe/fe-core/.../datasource/{hive,iceberg,paimon,hudi,trinoconnector,maxcompute,...},
reached from the catalog and planner layers through switch-case and
instanceof *ExternalTable. Adding or upgrading a source meant changing FE core, and
every source's client stack lived on the single FE classpath.

After: each source ships as a self-contained plugin under fe/fe-connector/ (hive, hms,
iceberg, paimon, hudi, trino, maxcompute, jdbc, es), loaded child-first from
Config.connector_plugin_root. fe-core keeps only generic infrastructure — the
PluginDriven* catalog / database / table / scan-node bridge plus one per-statement
ConnectorMetadata funnel — and no compile-time knowledge of any specific source:
CatalogFactory routes on ConnectorProvider.getType() alone. Two validate-phase
gates keep it that way: a connector may not import fe-core internals
(build-support/check-fe-connector-imports.sh), and only PluginDrivenMetadata may
call Connector#getMetadata (build-support/check-fe-core-metadata-funnel.sh).

Catalogs persisted by an older FE (IcebergExternalCatalog, PaimonExternalDatabase,
...) still deserialize and migrate transparently to the plugin-driven types.

Release note

External catalogs (hive, hms, iceberg, paimon, hudi, trino-connector, maxcompute, jdbc,
es) are now served by connector plugins, shipped as zips under fe/plugins/connector/
(connector_plugin_root). Existing catalogs keep working across upgrade; no DDL change
is required.

The default of trino_connector_plugin_dir moves from plugins/connectors to
plugins/trino_plugins; the old directory is still read as a fallback.

The deprecated lakesoul catalog type is removed — creating or replaying one now fails
with "Lakesoul catalog is no longer supported".

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:

    Covered by the connector modules' own unit tests, fe-core unit tests (including a
    4.1.3 metadata upgrade-compatibility suite), the two gate self-test scripts under
    build-support/tests/, and the external_table_p0 / external_table_p2 regression
    suites.

  • Behavior changed:

    • No.
    • Yes. See the release note: plugin-based packaging and loading of external
      catalogs, the trino_connector_plugin_dir default, and the removal of the
      lakesoul catalog type. Everything else is held to behavior parity.
  • Does this need documentation?

    • No.
    • Yes. Doc PR to follow: connector plugin layout, connector_plugin_root,
      the trino_connector_plugin_dir default, and the lakesoul removal.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@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
TPC-H: Total hot run time: 29382 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit b8d64260e0c80776d2b7bc42c2905102e1c36375, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17600	4122	4064	4064
q2	q3	10738	1408	822	822
q4	4687	480	348	348
q5	7593	892	598	598
q6	188	174	138	138
q7	781	871	638	638
q8	9346	1621	1642	1621
q9	5747	4539	4492	4492
q10	6781	1799	1537	1537
q11	427	277	255	255
q12	635	433	308	308
q13	18127	3346	2821	2821
q14	270	265	242	242
q15	q16	822	804	719	719
q17	1008	955	971	955
q18	7203	5704	5453	5453
q19	1325	1248	1038	1038
q20	516	427	267	267
q21	6215	2870	2742	2742
q22	497	376	324	324
Total cold run time: 100506 ms
Total hot run time: 29382 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	5158	4837	4840	4837
q2	q3	4877	5292	4699	4699
q4	2180	2197	1397	1397
q5	4773	4994	4655	4655
q6	231	176	129	129
q7	1817	1796	1553	1553
q8	2410	2157	2084	2084
q9	7960	7672	7324	7324
q10	4726	4639	4220	4220
q11	538	380	351	351
q12	728	740	534	534
q13	3010	3345	2762	2762
q14	268	281	246	246
q15	q16	689	708	611	611
q17	1283	1250	1255	1250
q18	7327	6947	6789	6789
q19	1127	1091	1115	1091
q20	2220	2217	1956	1956
q21	5299	4576	4535	4535
q22	532	470	406	406
Total cold run time: 57153 ms
Total hot run time: 51429 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 169289 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 b8d64260e0c80776d2b7bc42c2905102e1c36375, data reload: false

query5	4329	637	506	506
query6	458	208	183	183
query7	4818	557	302	302
query8	365	224	201	201
query9	8764	4095	4064	4064
query10	463	315	271	271
query11	5965	2355	2129	2129
query12	161	106	102	102
query13	1269	653	433	433
query14	6468	5426	5124	5124
query14_1	4415	4438	4402	4402
query15	205	198	181	181
query16	1041	474	471	471
query17	1128	733	601	601
query18	2721	479	353	353
query19	211	193	149	149
query20	115	108	111	108
query21	216	144	128	128
query22	13764	13618	13390	13390
query23	17345	16424	16235	16235
query23_1	16225	16282	16344	16282
query24	7550	1758	1312	1312
query24_1	1304	1278	1342	1278
query25	584	470	416	416
query26	1309	317	175	175
query27	2718	572	332	332
query28	4502	2036	2048	2036
query29	1085	655	525	525
query30	323	242	205	205
query31	1143	1075	969	969
query32	105	64	61	61
query33	541	337	268	268
query34	1214	1172	640	640
query35	757	797	693	693
query36	1411	1358	1273	1273
query37	151	106	91	91
query38	3223	3186	3078	3078
query39	936	915	894	894
query39_1	897	873	905	873
query40	225	126	104	104
query41	69	64	63	63
query42	98	100	95	95
query43	332	344	284	284
query44	
query45	207	188	179	179
query46	1136	1199	722	722
query47	2341	2346	2206	2206
query48	413	415	292	292
query49	642	475	367	367
query50	950	349	257	257
query51	4346	4307	4286	4286
query52	87	91	79	79
query53	248	266	200	200
query54	266	224	202	202
query55	86	77	72	72
query56	245	220	230	220
query57	1421	1418	1312	1312
query58	245	213	199	199
query59	1593	1655	1386	1386
query60	281	254	236	236
query61	164	164	157	157
query62	703	641	588	588
query63	239	191	189	189
query64	2526	789	635	635
query65	
query66	1742	464	342	342
query67	29789	29774	29479	29479
query68	
query69	421	301	262	262
query70	977	940	954	940
query71	309	222	209	209
query72	2965	2685	2421	2421
query73	871	779	431	431
query74	5140	4995	4750	4750
query75	2667	2603	2241	2241
query76	2320	1167	816	816
query77	362	374	292	292
query78	12396	12530	11905	11905
query79	1415	1052	770	770
query80	739	493	400	400
query81	475	284	242	242
query82	564	171	123	123
query83	355	276	251	251
query84	
query85	912	527	443	443
query86	402	313	298	298
query87	3417	3367	3171	3171
query88	3634	2733	2748	2733
query89	420	373	330	330
query90	1874	180	184	180
query91	182	167	140	140
query92	69	64	61	61
query93	1460	1462	940	940
query94	603	360	286	286
query95	671	379	442	379
query96	1044	784	351	351
query97	2704	2698	2552	2552
query98	215	205	202	202
query99	1175	1212	1028	1028
Total cold run time: 251873 ms
Total hot run time: 169289 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 58.71% (428/729) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 15.23% (111/729) 🎉
Increment coverage report
Complete coverage report

@morningman
morningman force-pushed the branch-catalog-spi branch 2 times, most recently from f09b6df to e9c5b3e Compare June 16, 2026 23:02
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 15.94% (205/1286) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17667	4024	3913	3913
q2	2027	306	188	188
q3	10343	1406	800	800
q4	4680	462	334	334
q5	7536	871	578	578
q6	174	164	134	134
q7	804	823	620	620
q8	9313	1513	1522	1513
q9	5791	4548	4530	4530
q10	6792	1786	1533	1533
q11	451	273	239	239
q12	632	417	290	290
q13	18104	3278	2774	2774
q14	263	259	241	241
q15	q16	783	766	698	698
q17	941	954	1016	954
q18	6943	5919	5558	5558
q19	1296	1215	1090	1090
q20	488	405	264	264
q21	6243	2768	2706	2706
q22	454	382	320	320
Total cold run time: 101725 ms
Total hot run time: 29277 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5104	4716	4766	4716
q2	334	371	238	238
q3	4867	5381	4718	4718
q4	2073	2137	1386	1386
q5	4832	4921	4605	4605
q6	237	176	125	125
q7	1926	1734	1541	1541
q8	2424	2117	2083	2083
q9	7837	7400	7489	7400
q10	4724	4682	4206	4206
q11	528	382	354	354
q12	730	735	521	521
q13	3041	3372	2790	2790
q14	262	293	250	250
q15	q16	675	701	619	619
q17	1280	1260	1240	1240
q18	7172	6756	6813	6756
q19	1168	1096	1086	1086
q20	2236	2216	1936	1936
q21	5295	4598	4428	4428
q22	523	459	419	419
Total cold run time: 57268 ms
Total hot run time: 51417 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 174666 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 e5959e1b53df3fb118edf5635e98cd6f258faea0, data reload: false

query5	4301	613	483	483
query6	433	195	170	170
query7	4824	571	309	309
query8	370	205	193	193
query9	8741	4021	4011	4011
query10	458	306	261	261
query11	5958	2351	2147	2147
query12	161	106	95	95
query13	1248	579	436	436
query14	6403	5383	5033	5033
query14_1	4375	4391	4309	4309
query15	208	196	176	176
query16	970	435	361	361
query17	908	678	551	551
query18	2436	470	336	336
query19	194	179	139	139
query20	110	102	101	101
query21	211	138	116	116
query22	13609	13680	13349	13349
query23	17458	16600	16216	16216
query23_1	16196	16343	16203	16203
query24	7475	1738	1297	1297
query24_1	1349	1316	1315	1315
query25	534	423	362	362
query26	1311	323	161	161
query27	2686	592	335	335
query28	4427	2039	2032	2032
query29	1065	604	476	476
query30	313	251	201	201
query31	1140	1081	957	957
query32	108	61	62	61
query33	547	318	264	264
query34	1163	1108	701	701
query35	764	778	692	692
query36	1420	1377	1254	1254
query37	161	106	95	95
query38	3220	3142	3070	3070
query39	962	926	890	890
query39_1	872	879	862	862
query40	227	126	106	106
query41	68	72	67	67
query42	97	97	95	95
query43	320	330	286	286
query44	1428	776	789	776
query45	194	189	182	182
query46	1086	1204	752	752
query47	2362	2349	2238	2238
query48	416	401	292	292
query49	632	483	354	354
query50	1099	369	268	268
query51	4328	4346	4254	4254
query52	90	90	77	77
query53	248	261	189	189
query54	288	226	215	215
query55	80	82	72	72
query56	238	231	243	231
query57	1421	1432	1323	1323
query58	243	223	218	218
query59	1580	1619	1449	1449
query60	304	255	247	247
query61	177	177	173	173
query62	709	662	586	586
query63	231	196	204	196
query64	2593	755	585	585
query65	4884	4782	4781	4781
query66	1814	454	324	324
query67	29938	29715	28989	28989
query68	3225	1537	995	995
query69	396	307	253	253
query70	1071	970	970	970
query71	291	235	206	206
query72	2844	2660	2351	2351
query73	878	786	446	446
query74	5121	4965	4773	4773
query75	2621	2595	2233	2233
query76	2298	1170	817	817
query77	355	377	288	288
query78	12370	12572	11847	11847
query79	1407	1142	753	753
query80	1266	456	380	380
query81	520	274	237	237
query82	582	150	118	118
query83	344	281	247	247
query84	303	145	114	114
query85	894	502	409	409
query86	421	303	288	288
query87	3358	3359	3216	3216
query88	3713	2756	2787	2756
query89	424	383	331	331
query90	1889	180	177	177
query91	172	162	135	135
query92	65	61	54	54
query93	1487	1370	890	890
query94	709	361	300	300
query95	673	374	338	338
query96	1080	805	352	352
query97	2732	2688	2594	2594
query98	217	211	203	203
query99	1162	1136	1042	1042
Total cold run time: 261477 ms
Total hot run time: 174666 ms

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.10	0.05	0.05
query3	0.25	0.14	0.13
query4	1.60	0.13	0.14
query5	0.23	0.22	0.22
query6	1.27	1.04	1.06
query7	0.03	0.01	0.00
query8	0.07	0.04	0.04
query9	0.38	0.31	0.32
query10	0.55	0.55	0.55
query11	0.20	0.15	0.14
query12	0.18	0.15	0.14
query13	0.48	0.48	0.47
query14	1.03	1.03	1.04
query15	0.63	0.60	0.59
query16	0.32	0.32	0.32
query17	1.11	1.10	1.05
query18	0.24	0.21	0.21
query19	2.08	1.95	1.88
query20	0.02	0.02	0.02
query21	15.46	0.23	0.13
query22	4.78	0.05	0.05
query23	16.11	0.30	0.12
query24	2.94	0.38	0.31
query25	0.11	0.05	0.04
query26	0.72	0.20	0.15
query27	0.04	0.04	0.04
query28	3.49	0.91	0.56
query29	12.52	4.26	3.45
query30	0.27	0.15	0.16
query31	2.77	0.61	0.31
query32	3.23	0.59	0.50
query33	3.13	3.20	3.16
query34	15.61	4.20	3.59
query35	3.52	3.54	3.52
query36	0.57	0.42	0.42
query37	0.09	0.07	0.07
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.19	0.16	0.14
query41	0.08	0.03	0.02
query42	0.04	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 96.57 s
Total hot run time: 25.17 s

@morningman
morningman force-pushed the branch-catalog-spi branch 3 times, most recently from 291d192 to 494ec17 Compare July 3, 2026 13:03
@morningman
morningman requested a review from luwei16 as a code owner July 5, 2026 14:41
@morningman
morningman force-pushed the branch-catalog-spi branch from a653478 to 3ba75b7 Compare July 7, 2026 09:35
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 59.99% (1870/3117) 🎉
Increment coverage report
Complete coverage report

@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@github-actions

Copy link
Copy Markdown
Contributor

PR approved by anyone and no changes requested.

@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 59.06% (25571/43298)
Line Coverage 43.13% (256308/594221)
Region Coverage 38.84% (203123/523034)
Branch Coverage 40.15% (92575/230548)

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 48.56% (3884/7999) 🎉
Increment coverage report
Complete coverage report

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@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.60% (31925/42228)
Line Coverage 60.22% (355466/590253)
Region Coverage 56.80% (298064/524780)
Branch Coverage 58.20% (134203/230588)

@morningman
morningman merged commit a73cdc0 into master Jul 30, 2026
36 of 38 checks passed
@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 17.32% (1388/8012) 🎉
Increment coverage report
Complete coverage report

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes. This cutover is broadly decomposed and its current architecture gates pass, but I found 12 blocking correctness, lifecycle, classloading, and compatibility issues in the inline comments. The PR merged while this review was running, so these are post-merge follow-up blockers against the reviewed head f1d6f1f.

Critical checkpoint conclusions:

  • Goal and scope: the connector extraction/cutover is substantially implemented, but the 1,680-file transition is not safe as merged because production routes activate unresolved transaction and classloader boundaries.
  • Data correctness and error handling: Iceberg v3 lineage names can become ambiguous real/synthetic columns; HUDI mixed-version requests and Trino commit failures can report success after failure; replay catches fatal JVM errors.
  • Concurrency and lifecycle: the Iceberg async split pump loses the plugin TCCL, external-write rollback ownership is removed before failure cleanup, and Trino scan/handle transactions have broken ownership.
  • Configuration and compatibility: Trino's untouched default no longer finds legacy plugin layouts; old-FE/new-BE HUDI metadata compatibility is broken; Paimon/Jindo/AWS plugin closures are incomplete or multiply owned.
  • Parallel paths and FE/BE protocol: eager, partition-batch, streaming, write, and JNI/native paths were traced. The separate batch-callback suspicion has no current shipped resource owner, but the streaming and retained-HUDI branches above are real.
  • Persistence, failover, and writes: legacy image/journal subtype fixtures are extensive and no additional mapping defect was found; the replay error boundary and failed-commit rollback ordering remain unsafe.
  • Testing and observability: negative coverage is missing for these failure modes, and several paths silently return/log success. The review-only instructions prohibited builds and test execution. I ran check-fe-connector-imports.sh and check-fe-core-metadata-funnel.sh; both passed.
  • Performance and memory: no separate algorithmic regression was substantiated, but leaked Trino transactions and failed source cleanup are resource-lifecycle regressions.

User focus: no additional focus was provided, so the full PR was reviewed.

LOG.debug("Plugin-driven transaction begun with SPI ConnectorTransaction: {}", txnId);
return txnId;
}

@Override
public void commit(long id) throws UserException {
PluginDrivenTransaction txn = transactions.remove(id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Keep the transaction reachable until commit failure cleanup runs

commit() removes the wrapper before calling the connector, and the wrapper also closes the connector transaction in finally. If that call throws, AbstractInsertExecutor invokes onFail() and calls rollback(txnId), but the map no longer contains the transaction, so rollback is a no-op. This is reachable in the current Hive connector: HiveConnectorTransaction.commit() calls finishInsertTable() before creating its self-rolling-back HmsCommitter, and explicitly relies on the engine's subsequent rollback to abort pending multipart uploads when that phase fails. Please retain the transaction through the failure path or roll it back inside commit() before closing and deregistering it.

VLOG_CRITICAL << "MetaScanner::_fetch_metadata";
TFetchSchemaTableDataRequest request;
switch (meta_scan_range.metadata_type) {
case TMetadataType::HUDI:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Preserve the old-FE HUDI metadata request during rolling upgrades

TMetadataType.HUDI and TMetaScanRange.hudi_params remain on the wire as deprecated values, so an old FE can still send this request to a newly upgraded BE. With this switch arm removed, the retained enum falls into the default branch, which sets EOS and returns OK; hudi_meta() therefore reports a valid empty timeline instead of its rows. Please keep this request bridge for the mixed-version support window (or at least fail the retained enum explicitly) and cover the old-FE/new-BE path.

catalogType, props, new DefaultConnectorContext(name, catalogId));
} catch (RuntimeException | Error e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Do not turn fatal JVM errors into degraded catalogs

The fallback is meant for legacy validation failures and a missing connector class, but catching Error also swallows OutOfMemoryError, StackOverflowError, ThreadDeath, AssertionError, and unrelated VM/linkage failures during replay. Continuing startup after those failures can leave FE in an unsafe state while misreporting the cause as one degraded catalog. Please catch only the expected runtime/configuration and narrowly selected linkage conditions (for example NoClassDefFoundError) and let fatal/invariant errors propagate.

// Pull ranges with backpressure (needMoreSplit) and pump them one at a time, exactly like
// legacy doStartSplit. The bounded SplitAssignment queue throttles the lazy source so FE
// heap stays bounded for million-file scans.
while (splitAssignment.needMoreSplit() && source.hasNext()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Keep the plugin TCCL pinned while consuming the lazy split source

The pin here covers only streamSplits() construction. The SPI explicitly requires heavy planning to be deferred until consumption, and Iceberg's source opens tasks.iterator() and maps file tasks inside hasNext(); its manifest-cache path can reach ManifestFiles.read(...) on this same engine pump thread. next() and close() also execute plugin/SDK code. All of those calls run after onPluginClassLoader has restored the app TCCL (Iceberg's separate worker-pool pin does not pin this engine thread), reintroducing the reflective/service-loading split-brain this helper is intended to prevent; a resulting LinkageError also bypasses catch (Exception). Please pin the full source lifecycle and route its failures through SplitAssignment.

IcebergColumnChange change = toAddColumnChange(column);
try {
context.executeAuthenticated(() -> {
catalogOps.addColumn(iceHandle.getDbName(), iceHandle.getTableName(), change, position);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Restore the v3 row-lineage name guard on schema mutations

These ALTER paths no longer apply the removed validateRowLineageColumnMutation check. On a v3 table, ADD COLUMN _row_id BIGINT (or renaming a field to _last_updated_sequence_number) can therefore commit a normal Iceberg field with an ordinary ID; buildTableSchema() then unconditionally appends Doris's synthetic field with the same name and its reserved ID. The resulting duplicate names make slot/handle binding ambiguous and can treat user data as the row-lineage passthrough field. Please restore the case-insensitive, format-version-aware guard across every top-level mutation route and retain the former negative coverage.

Optional<ConnectorExpression> filter,
long limit) {
TrinoTableHandle trinoHandle = (TrinoTableHandle) handle;
public List<ConnectorScanRange> planScan(ConnectorSession session, ConnectorScanRequest request) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] End every Trino scan transaction

This cutover makes this provider the production Trino scan path, but the transaction started below is never committed or rolled back. cleanupQuery() only closes the metadata query scope; the provider inherits the no-op releaseReadTransaction(), and BE can only close its own page source after consuming the serialized handle. Each SELECT therefore leaves connector transaction state behind, and a planning exception leaks it immediately. Please register the transaction by query ID, release it exactly once from the existing query-finish hook (with an immediate rollback on pre-registration/planning failure), and cover success, failure, and cancellation.

dbName, tableName, trinoHandle,
columnHandleMapBuilder.buildOrThrow(),
columnMetadataMapBuilder.buildOrThrow()));
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Keep Trino handles with the transaction that created them

The opaque table and column handles returned here were created under txn, but this finally commits that transaction before the TrinoTableHandle escapes. Later schema calls, pushdowns, and split planning open unrelated transactions and reuse the saved handles; connectors are not required to make their handles valid across transaction boundaries, and the removed legacy path kept metadata, handles, and transaction paired. Please perform discovery/refinement/split planning in one query-scoped transaction, or reacquire every handle under the scan transaction before using it.

* connector's transaction manager. A release failure is logged, never rethrown, so it cannot mask
* the real exception a {@code finally} block runs after.
*/
private void releaseQuietly(io.trino.spi.connector.ConnectorTransactionHandle txn) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Propagate release failures when the metadata call succeeded

releaseQuietly() is also used after successful metadata calls, so a connector commit() failure is logged and the caller still receives a successful list/schema/handle result while the transaction may remain live. Avoiding a finally exception from masking a primary failure is useful only when a primary failure exists. Please propagate the release error on the success path; when already unwinding, preserve the original exception and attach/log the release failure as secondary.

hadoop-aws 3.4.2 wires ApacheHttpClient and it is only test-scoped in s3's closure. Versions are
BOM-managed (awssdk ${awssdk.version}, matching fe-core). software.amazon.awssdk MUST stay
child-first (NOT in the parent-first allowlist) — that separate child copy of the SDK is the
whole point. (STS/assumed-role would need software.amazon.awssdk:sts added the same way.) -->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Include STS in Paimon's child-first AWS closure

The supported s3.role_arn / AWS_ROLE_ARN path selects Hadoop's AssumedRoleCredentialProvider, but this isolated plugin bundles child copies of hadoop-aws, s3, sdk-core, and auth while omitting the STS module that this comment says AssumeRole needs. STS then falls back to the parent and links against parent SDK types while the provider uses child SDK types (or is simply absent in another distribution), so role-based Paimon S3 access fails at runtime. Please bundle the same BOM-aligned software.amazon.awssdk:sts closure used by the isolated Iceberg/S3 plugins and cover it through the isolated loader.

@@ -253,10 +264,34 @@ private PluginHandle<F> loadFromPluginDir(Path pluginDir, ClassLoader parent, Cl
}
// Re-load and instantiate the factory class from the runtime classloader so that it
// has full access to lib/ classes (e.g. CosFileSystemProvider needs S3 classes).
Class<?> discoveredClass;
try {
discoveredClass = classLoader.loadClass(factoryClassName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Contain unloadable factories within their plugin directory

loadClass() runs before the API-version check, but this catch does not cover NoClassDefFoundError, UnsupportedClassVersionError, or other LinkageErrors. asSubclass() can also throw ClassCastException, and static initialization during construction can throw a linkage error. Since loadAll() catches only PluginLoadException, one incompatible plugin escapes the documented per-directory boundary and prevents all later plugins from loading. Please read admission metadata before defining the factory where possible and translate plugin-attributable linkage/type failures into the staged load failure while still allowing fatal VM errors to propagate.

morningman added a commit that referenced this pull request Jul 31, 2026
…y the harness (#66195)

### What problem does this PR solve?

FE unit tests dominate CI wall time, and most of that time is not spent
running test logic.

Full attribution of one `fe-core` surefire window (4092s x 12 forks =
**49,104 fork-seconds**, 86% of an 79.7-min build):

| component | fork-seconds | share | nature |
|---|---|---|---|
| JVM lifecycle *between* classes (1219x) | 17,317 | 35.3% | fixed
overhead, proportional to class count |
| non-FE classes: real test execution | 13,259 | 27.0% | **real work** |
| FE startup + teardown (304x) | 7,066 | 14.4% | fixed overhead,
proportional to FE-starting classes |
| FE classes: class loading before FE start | 4,463 | 9.1% | fixed
overhead |
| FE classes: real test execution | 3,797 | 7.7% | **real work** |
| non-FE classes: class-level setup | 3,172 | 6.5% | fixed overhead |

**Real work 17,055s (34.7%) vs fixed overhead 32,019s (65.2%).**
Unattributed: 30s.

The overhead is per *class*, so the only lever with real magnitude is
**reducing class count by merging test classes**. Eliminating one class
is worth **55.2 fork-seconds if it starts an FE** (304 such classes)
versus **16.0 if it does not** (915 classes) — FE-starting classes are
3.5x more valuable to merge.

### What is changed?

Six commits, in dependency order:

1. **Avoid a 10s heartbeat wait in every FE unit test class.**
`Daemon.run()` runs one cycle before sleeping, so `HeartbeatMgr`'s first
cycle completes before `createDorisCluster()` registers a backend;
`checkBEHeartbeat()` then waits a full `heartbeat_interval_second`
(default 10). Sets the interval to 1 before the `Env` singleton is
created, and makes `checkBEHeartbeatStatus()` check-then-sleep at 20ms
granularity instead of sleeping 1s first. Timeout budget unchanged.

2. **Merge 12 tiny nereids test classes into 3 suites.**

3. **Give `DistributeHintTest` assertions and cut its runtime by 95%.**
It enumerated a large parameter space while asserting almost nothing;
now it asserts the distribution actually chosen and covers the space at
a sane size.

4. **Restore spied `Env` fields after every test method**, to stop
unbounded spy nesting leaking across methods.

5. **Merge six command privilege tests into one suite.**

6. **Migrate 25 legacy `UtFrameUtils` test classes to
`TestWithFeService`.** This one saves no time by itself — it is the
precondition for merging, since only classes on the same harness can
share an FE.

### Measurements (CI, build 1009445 unless noted)

| change | measured | note |
|---|---|---|
| heartbeat fix | -3.7s/class, **within noise** | **not the lever** —
see caveat below |
| merge 12 -> 3 classes | 614.4s -> 168.5s | -72.6% |
| `DistributeHintTest` | 458.7s -> 64.2s | -86% |
| merge 6 -> 1 class | 302.8s -> 63.9s | -78.9%, 13 test cases unchanged
|
| migrate 25 classes | `DropTableTest` 4.483s -> 4.566s | no gain by
design; enables merging |

The three verified items save roughly **840 test-seconds** in total.

**Caveat, stated plainly:** at ~7x effective parallelism, and with
per-build noise of 2 sigma = 151s, that lands as only about **2 minutes
of wall clock and is partly masked by noise**. Do not expect the
total-seconds number in the build summary to move cleanly. The
measurement method that does work is per-class comparison against an
unaffected control group, not the build total. The heartbeat fix in
commit 1 in particular looked large in local single-fork measurement and
turned out to be noise-level on CI — it is kept because it is correct
and harmless, not because it is a win.

Full-suite result on build 1009445: **SUCCESS, 8773 passed, 0 failed, 1
muted.** The muted one is `ForwardToMasterTest.testAddBeDropBe`
(ClassCastException), which is known upstream issue #66004 and unrelated
to this branch.

### Rebase note

Rebased onto `af6dcff9051` today. Two files conflicted with upstream,
both against commit 6:

- `FrontendServiceImplTest` — #64304 moved `MCTransaction` /
`MaxComputeExternalCatalog` out of `fe-core`; upstream's
`WriteBlockAllocatingTransaction` mock kept verbatim.
- `DynamicPartitionTableTest` — #65219 added 1478 lines of TIMESTAMPTZ
tests. Resolved by taking upstream's file whole and replaying the
harness migration onto it, then diffing against upstream to confirm only
harness lines moved. All 241 `Assert.*` calls left untouched.

Post-rebase verification: `test-compile` clean, `checkstyle:check`
clean, and the two conflicted classes run **69/69 passing, 0 skipped** —
counts matching upstream's annotation counts exactly.

### A note for reviewers on how this was verified

The JUnit4 -> JUnit5 migration in commit 6 has two failure modes that
**pass silently** rather than erroring:

- `@Rule ExpectedException` is ignored outright by JUnit5, turning six
exception tests into tests that assert nothing. Converted to
`ExceptionChecker.expectThrowsWithMsg`, which matches JUnit4 semantics
(`isInstance` for subclasses, `contains` for messages).
- JUnit4 `Assert.assertEquals(msg, expected, actual)` versus JUnit5
`Assertions.assertEquals(expected, actual, msg)` have **reversed
argument order**. When all three arguments are Strings this compiles
fine and silently compares the wrong things. 36 such call sites exist
across the migrated classes; message-first overloads were identified by
argument *count*, not by whether the first argument looks like a string.

Because of this, `BUILD SUCCESS` is not sufficient evidence for these
commits. Every migrated class was accepted only after its `Tests run:`
count matched the pre-migration count exactly. Reviewers checking this
PR should apply the same standard.

### Release note

None

### Check List

- [x] Test
- [x] Regression test — not applicable, this changes only the FE unit
test harness and test classes
- [x] Unit Test — full FE UT suite green on CI (build 1009445: 8773
passed, 0 failed); post-rebase spot check 69/69 on the two conflicted
classes
- [x] Behavior changed: No (test-only, no production code paths altered)
- [x] Does this need documentation? No
morningman added a commit that referenced this pull request Jul 31, 2026
…66303)

### What problem does this PR solve?

Issue Number: close #xxx

Related PR: #64304

Problem Summary:

Follow-up cleanup for #64304. After external catalogs moved behind the
connector plugin SPI, fe-core still declared a name for every data
source it no longer knows anything about. This removes those names:

- **`TableIf.TableType`**: the per-source `*_EXTERNAL_TABLE` constants
(`HMS`, `ES`, `JDBC`, `ICEBERG`, `PAIMON`, `MAX_COMPUTE`, `HUDI`,
`TRINO_CONNECTOR`, `LAKESOUl`) and the deprecated internal-catalog
`ICEBERG` / `HUDI`. An external table served by a connector plugin is a
`PLUGIN_EXTERNAL_TABLE`; the source's own name is answered by
`PluginDrivenExternalCatalog#getDisplayEngineName`, never by a mapping
held in fe-core.
- **`TableFormatType`**: `hive`, `iceberg`, `hudi`, `paimon`,
`max_compute`, `transactional_hive`, `lakesoul`, `trino_connector`. A
connector names its own format string through
`ConnectorScanRange#getTableFormatType()`, which `PluginDrivenScanNode`
forwards to BE verbatim. Only `tvf` and `remote_doris` are still
produced by fe-core itself. The enum is not persisted anywhere — it only
builds the thrift string.
- **`InitDatabaseLog.Type`**, and with it `ExternalDatabase#dbLogType`,
a field that was assigned and never read. `OP_INIT_EXTERNAL_DB` has been
ignored on replay since 4.0 and `EditLog#logInitExternalDb` has no
callers, so nothing writes one any more; the payload class stays only so
`JournalEntity` can still consume such an entry out of an old journal.
- **`InitCatalogLog.Type.HUDI`**, which was never produced: hudi tables
have always lived in an hms catalog and no `HudiExternalCatalog` ever
existed.
- **`Database#discardHudiTable`**, dead once `TableType.HUDI` is gone.

The remaining `InitCatalogLog.Type` values are kept on purpose. That
enum is also the type of the persisted `ExternalCatalog#logType` field,
which `PluginDrivenExternalCatalog#gsonPostProcess` reads to backfill
the catalog type for the resource-backed catalogs (`es`, `jdbc`) that
never persisted one. Deleting a name there would make it deserialize to
`null` and lose that catalog's type on upgrade.

**Upgrade compatibility.** An image written before the cutover still
carries the old `TableType` names. It stays readable because the
persisted table class is remapped by `GsonUtils`' compatible-subtype
registry, and the stale `type` string deserializes to `null` — GSON
returns `null` for an enum name it does not know rather than throwing —
which `PluginDrivenExternalTable#gsonPostProcess` then normalizes to
`PLUGIN_EXTERNAL_TABLE`, the same normalization it already applied to a
recognized legacy name.

Nothing in the compiler enforces that two-step property, and a
regression would surface not as a build error but as an FE replaying a
persisted external table with a `null` type. So the new
`LegacyExternalTableTypeReplayTest` pins it for every deleted name,
deliberately spelled as string literals so the constants cannot come
back just to keep a test compiling.
morningman pushed a commit that referenced this pull request Aug 4, 2026
### What problem does this PR solve?

Related PR: #64304 (catalog SPI)

Related PR: #65126

Problem Summary: External metadata caching currently has parallel
implementations in FE core and connector
modules. Cache policy, statistics, invalidation, lifecycle, and
concurrency behavior are therefore duplicated,
while connector implementations still depend on FE-owned concepts.

This PR introduces an implementation-free MetaCache SPI module and a
shared Caffeine-backed runtime module.
Common lifecycle, entry, registry, statistics, invalidation, and ID/name
coordination are moved into these
modules. FE core retains only FE-specific catalog routing, configuration
injection, refresh orchestration,
edit-log integration, and schema validation. Hive, HMS, Iceberg, Paimon,
and MaxCompute consumers are migrated
to the shared implementation.

The refactor preserves the existing connector constructor policy and the
guarded bulk-load contract. A guarded
bulk publication participates in the same short generation protocol as
manual miss loads, exact-key actions, and
asynchronous refreshes, while REFRESH/flush invalidation continues to
win without holding a publication monitor
during external I/O.
morningman added a commit that referenced this pull request Aug 4, 2026
…ng (#66369)

### What problem does this PR solve?

Related PR: #64304 (catalog SPI)

Problem Summary:

A connector never decides which columns to read — it renders whatever
list `ConnectorScanRequest.getColumns()` carries. The jdbc connector
turns that list verbatim into the remote `SELECT` list and falls back to
`SELECT *` when it is empty (`JdbcQueryBuilder#buildQuery`). So column
pruning for every plugin-driven external scan rests entirely on
`PluginDrivenScanNode#buildColumnHandles()`, which intersects the
connector's column handles with this scan's tuple slots.

That method had **no direct coverage**, and its failure mode (projecting
more columns than the query needs) is a pure performance regression that
no result-comparing test can observe. The existing jdbc explain
assertions all pass a column list and assert those same columns are
present; none of them can fail on an over-wide projection they did not
anticipate.

This PR started as coverage for that gap. The new coverage immediately
found a real bug, so it now carries the fix as well.

**1. The bug: the connector's scan properties are computed before column
pruning**

A plugin-driven scan asks its connector for one property bundle — the
jdbc remote `SELECT`, per-column dictionaries, file format, path
partition keys — and caches it (`cachedPropertiesResult` /
`scanNodeProperties`).

That cache is first filled from `init()`:

```
PhysicalPlanTranslator#getPlanFragmentForPhysicalFileScan
  -> scanNode.init()
     -> FileQueryScanNode#doInitialize -> initSchemaParams -> getPathPartitionKeys()
        -> PluginDrivenScanNode#getPathPartitionKeys -> getOrLoadScanNodeProperties()
```

`init()` runs while the translator is still translating this scan —
strictly **before** the project above it prunes the tuple down to the
columns the query reads (`updateScanSlotsMaterialization`). Everything
the connector derives from the projection at that point therefore
describes the **full table schema**.

The only thing that dropped the cache was `convertPredicate()`, and only
when there was a conjunct to push down. Queries with a `WHERE` clause
were rebuilt from the pruned tuple by accident; filter-less ones kept
the pre-pruning bundle. Result:

```sql
-- doris_test.test1 has 12 columns
explain select count(*) from test1;
-- before: QUERY: SELECT `k1`, `k2`, ..., `k12` FROM `doris_test`.`test1`
-- after:  QUERY: SELECT `k1` FROM `doris_test`.`test1`
```

This holds for **every** `WHERE`-less query on any plugin-driven
external table, not just `count(*)`.

The scan itself was not affected — `getSplits()` rebuilds the column
handles from the final tuple, so the query actually sent to the source
was already pruned. What was wrong is the reported remote query, and
anything else a connector derives from the projection through this
bundle (`populateScanLevelParams`, `getFileAttributes`). Reviewers of
the iceberg connector may want to check the field-id dictionary applied
in `IcebergScanPlanProvider#populateScanLevelParams`, which is built
from the requested columns and, on a filter-less query, was built over
the full schema.

Fix: drop the cache in `doFinalize()`, the first point at which the
tuple is final. Every filtered query already exercises this rebuild path
today — including the second MVCC-snapshot / rewrite-scope pin it
implies — so the filter-less path is only being moved onto an
already-exercised path.

Why it survived since the SPI migration: all 13 remote-query assertions
in the tree filter, and a filter is exactly what used to hide this. The
one filter-less assertion that exists (`test_gbase_jdbc_catalog`,
commented out) expects the pruned single column, i.e. the behavior this
restores.

**2. The projection decision itself (`fe-core`)**

`PluginDrivenScanNodeColumnPruningTest` drives the real
`buildColumnHandles()` and pins:

- only tuple-slot columns are projected (3-column table, 1 requested →
exactly 1 handle);
- the order follows the slot order, not the connector's handle-map order
— the connector renders this list positionally;
- slots with no backing column, and slots with no matching handle, are
skipped rather than leaking into the list;
- an empty tuple projects nothing — the sole input that reaches the
connector's `SELECT *` fallback.

Every assertion was mutation-checked against the production method:
returning `allHandles.values()` kills 4 of the 5, and making the
unmatched-slot path fail loud unconditionally kills the 5th.

**3. Explain assertions (`external_table_p0`)**

Two additions to `test_mysql_jdbc_catalog`, both of which fail without
the fix above:

- a filter-less projection (`select k8 from test1`) — the shape no
existing assertion covered, and the most direct pin for the caching bug;
- `count(*)`, the one shape whose projection would otherwise go empty.
The engine keeps a single smallest slot
(`PhysicalPlanTranslator#updateScanSlotsMaterialization`) instead of
letting the tuple go empty, and an empty tuple is exactly what makes the
jdbc connector emit `SELECT *`. The assertion pins that the remote
select list stays one column wide and is not `*`; it deliberately does
**not** pin which column wins, since that is `getSmallestSlot`'s
business and tracks type widths.
hubgeter added a commit to hubgeter/doris that referenced this pull request Aug 5, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#64304

Problem Summary: Snapshot-pinned Iceberg scans built the full field-id schema dictionary with lower-cased top-level names while scan slots and identity partition keys retained the source names. Mixed-case partition columns therefore missed the BE schema mapping and could abort the backend. Preserve Iceberg top-level names in full-schema dictionaries, return query errors for malformed mappings instead of aborting, and align Hudi partition carriers with the Hudi lower-case schema convention.

### Release note

Fix mixed-case partition reads for Iceberg and Hudi external tables.

### Check List (For Author)

- Test: Unit Test
    - FE Iceberg, Hudi, Paimon, and Hive connector unit tests: 278 passed after rebase
    - BE Iceberg reader unit tests: passed before rebase; latest-master rerun did not reach test execution because the updated BE dependency graph required a full rebuild
- Behavior changed: Yes. Mixed-case external partition columns are matched consistently, and malformed schema mappings fail the query instead of terminating BE.
- Does this need documentation: No
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. dev/5.0.x reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Tracking][Catalog] Catalog SPI migration — decouple built-in connectors from FE core into loadable plugins

8 participants