This directory holds the connector plugin framework of the Doris FE: the contracts a connector implements, the shared infrastructure connectors reuse, and the connectors themselves. A connector teaches the FE to serve metadata, scans, and writes for an external source without fe-core knowing anything about that source.
Two companion documents divide the work with this one:
AGENTS.md(next to this file) — build/test recipes, the architecture gates, and the invariants that are not expressible as gates. Read it before changing code here.- Generic plugin machinery is NOT defined here: contracts live in
fe/fe-extension-spiand directory loading infe/fe-extension-loader; storage properties and filesystem access live infe/fe-filesystem. Each has its own README.
Three design rules shape everything in this directory:
- Connectors never import fe-core internals. Whatever a connector needs
from the engine must come through the SPI (
org.apache.doris.connector.*) or shared neutral types. A build gate enforces this (seeAGENTS.md). - fe-core parses no connector properties. Metadata-connection properties
are parsed inside connectors (usually via the metastore layer below);
storage properties belong to
fe-filesystem. - Generic engine code stays connector-agnostic. There is no
if (source == ...)in shared planning code; per-source behavior is reached only through the SPI, and optional behavior is opt-in via capabilities (see "Reading the API").
Roles only — one line each. For anything deeper, read the module's javadoc
(start at package-info.java) and pom comments; both are kept authoritative.
Contracts
| Module | Role |
|---|---|
fe-connector-spi |
The whole engine <-> connector contract, in both directions. What a connector implements: ConnectorProvider (discovery identity + factory), Connector, ConnectorMetadata with its Ops sub-interfaces, plus handle / pushdown / mvcc / scan / write / ddl / procedure / event / rest types. What the engine implements and hands down: ConnectorContext (including sibling-connector creation), ConnectorStorageContext, ConnectorSession, ConnectorConf. The javadoc here is the API reference. |
The two directions are one module on purpose: the boundary is bidirectional
(ConnectorProvider.create takes a ConnectorContext, and ConnectorContext
hands back a Connector), so splitting it by "who implements" would be
circular. Trino makes the same call with trino-spi. Contrast the metastore
layer below, where the split is acyclic and the usual api/spi convention holds.
Metastore layer (how connectors reach a metastore without hand-parsing endpoint properties)
| Module | Role |
|---|---|
fe-connector-metastore-api |
Typed carriers of metastore connection facts (HMS / REST / filesystem / JDBC flavors). |
fe-connector-metastore-spi |
MetaStoreProvider discovery SPI + the MetaStoreProviders dispatcher. Adding a backend = one provider + one META-INF/services line; no central switch. |
fe-connector-metastore-hms, -iceberg, -paimon |
The backend implementations (ServiceLoader providers), consumed by the connectors that need them. |
Shared infrastructure
| Module | Role |
|---|---|
fe-connector-cache |
Self-contained caching framework used by several connectors. No fe-core dependency; it is bundled into each consuming plugin, so shared third-party libraries stay at the consumers' lowest common version (see the version notes in consumer poms). |
fe-connector-hms-hive-shade |
Slim, relocated HMS metastore-client closure for connectors that speak HMS thrift. The pom comments say exactly what relocates where and why. |
fe-connector-paimon-hive-shade |
Paimon-private relocated HMS-thrift closure; same idea, different owner. |
Connectors
fe-connector-es, -jdbc, -maxcompute, -trino, -hms, -hive,
-iceberg, -paimon, -hudi — one module per source. Reactor build order is
dependency-driven; the ordering constraints (shades before their consumers,
metastore backends before connectors) are documented as comments in this
directory's pom.xml.
Startup. Env creates the engine-side ConnectorPluginManager
(fe-core, org.apache.doris.connector), which discovers providers in two
rounds: first ServiceLoader on the classpath (built-ins and tests), then
DirectoryPluginRuntimeManager over the plugin directories named by
Config.connector_plugin_root (production). Classpath providers win over
directory ones. At registration every provider passes an API-version gate
(ApiVersionGate) and a type-uniqueness check: a duplicate on the classpath
fails loud, a bad plugin in a directory is logged and skipped so one broken
plugin cannot stop FE startup.
CREATE CATALOG. Routing happens on ConnectorProvider.getType() — a
connector's globally unique, case-insensitive identity. CatalogFactory
(fe-core) asks ConnectorFactory for a standalone-catalog connector; on a
match the catalog is materialized as a PluginDrivenExternalCatalog, and on
no match the engine falls back to the catalog types it implements itself (the
error message lists the installed standalone types). The engine keeps no list
of accepted types: isStandaloneCatalogType() is the only switch. A
connector that returns false there is sibling-only — still registered and
reachable by a gateway connector through
ConnectorContext.createSiblingConnector (the hive connector owning a hudi
sibling is the live example), but never a catalog of its own.
A statement. Every metadata acquisition in fe-core funnels through
PluginDrivenMetadata — the single place allowed to call
Connector#getMetadata (an architecture gate enforces this). It memoizes
exactly one ConnectorMetadata instance per catalog on the statement's
ConnectorStatementScope and closes it deterministically at statement end.
Scan planning follows the same shape: the generic PluginDrivenScanNode
(fe-core) delegates all per-source planning to the connector's
ConnectorScanPlanProvider — note the interface lives in fe-connector-spi
(spi.scan), not in fe-core.
Classloading. Plugins load child-first, each carrying its own runtime
closure. Wherever engine code crosses into a plugin — or a bundled library
resolves classes by name, or a plugin spawns worker threads — the
thread-context classloader must be pinned to the plugin's classloader. The
concrete loci and the failure modes are listed in AGENTS.md.
Packaging. Each connector assembles a plugin zip via
src/main/assembly/plugin-zip.xml; FE loads the zips from
Config.connector_plugin_root at startup. Classpath built-ins exist for
tests and embedded use.
This document never lists SPI methods. The truth lives in code, behind four mechanisms:
- Javadoc is the API reference. Start at
ConnectorMetadata(and its Ops sub-interfaces) andConnectorProvider, both in fe-connector-spi. Every SPI method has a default body, so each sub-interface's class javadoc states its minimum implementation set, lifecycle, and threading rules. @ConnectorMustImplementis the machine-readable half of the minimum implementation set: it marks the default methods a connector is nevertheless expected to override, withwhennaming the capability that triggers the obligation. A unit test pins the annotated set, so promoting a method is a deliberate, reviewed change.- The recorded surface.
fe-connector-spi/src/test/resources/connector-metadata-methods.txtfreezes the public method surface ofConnectorMetadata;ConnectorMetadataSurfaceTestfails on any drift. Adding, removing, or moving SPI methods must regenerate this baseline in the same commit (run the test, copy the "actual" set from the failure message). That file is the API inventory — do not maintain one in prose. - Capabilities are opt-in.
ConnectorCapabilitydeclares optional switches consumed by static planning code; every constant states whether it is catalog-scoped (Connector#getCapabilities) or table-scoped (union with the table's own set — how a heterogeneous catalog admits only the tables that qualify). Write/sink traits live onConnectorWritePlanProvider(viaConnector#getWritePlanProvider), and scan-side opt-ins aredefault-falsesupports*methods onConnectorScanPlanProvider. Generic engine code never special-cases a source; if a connector does not opt in, the feature stays off.
Statement-scoped memoization has one framework-wide convention worth knowing
before you read connector code: ConnectorStatementScopes (fe-connector-spi)
keys per-statement values by (catalogId, db, table, queryId) plus a
connector-owned namespace constant prefixed with the connector's
getType(). Each connector guards its own prefix with a unit test
(EsStatementScope / EsStatementScopeTest is the minimal example).
Copy from the reference connector: fe-connector-paimon exercises the
whole framework (metastore layer, cache framework, a shade sibling, TCCL
pinning). fe-connector-es is the minimal contrast (REST source, no
metastore/shade/cache). For a write path, the richest example is
fe-connector-iceberg.
- Module. Create
fe-connector-<type>, register it in this directory'spom.xml<modules>(respect the ordering comments). Start the pom fromfe-connector-es(minimal) orfe-connector-paimon(full stack). - Type name. Read the
ConnectorProvider.getType()contract first: globally unique, case-insensitive, must not collide with an engine built-in catalog type. - Provider. Implement
ConnectorProvider(example:PaimonConnectorProvider) and register it insrc/main/resources/META-INF/services/org.apache.doris.connector.spi.ConnectorProvider. Providers must have a no-arg constructor and no fe-core dependency. - Connector + metadata. Implement
ConnectorandConnectorMetadata(example:PaimonConnector,PaimonConnectorMetadata). Scope the first cut by the minimum implementation set:@ConnectorMustImplementplus each Ops interface's class javadoc. - Metastore access. If the source's metadata sits behind HMS / REST /
JDBC / a filesystem, bind through the metastore layer
(
MetaStoreProviders; seePaimonCatalogFactoryfor the consumption pattern) instead of hand-parsing endpoint properties. A new backend flavor is a newMetaStoreProviderin afe-connector-metastore-<x>module plus its services line. - Property ownership. Metadata-connection properties are parsed in your
connector (or the metastore layer). Storage properties belong to
fe-filesystem. Do not add parsing to fe-core — rule 2 above. How to organize the keys themselves: see "Property Organization" below. - Caching. Reuse
fe-connector-cache(example:PaimonLatestSnapshotCache). Bundle the caching library into your plugin zip and keep shared third-party versions aligned with the other consumers (see the version notes infe-connector-paimon/pom.xml). Respect the authorization invariant inAGENTS.md: a cross-query cache must never serve metadata that would bypass per-user, load-time authorization. - Shading. If your client stack drags a conflicting closure (hive/thrift
is the recurring case), give it a dedicated relocated shade module; the
pom comments of
fe-connector-hms-hive-shadeandfe-connector-paimon-hive-shadedocument the pattern. - Handles and scan. Implement
ConnectorScanPlanProviderplus your handle types (example:PaimonScanPlanProvider,PaimonTableHandle,PaimonScanRange). Optional behavior goes throughsupports*/ConnectorCapabilityopt-ins only — the engine will not special-case your source. - Write path (if any). Implement the write Ops and
Connector#getWritePlanProvider; copy fromfe-connector-iceberg. - Statement-scoped memos. Follow the
ConnectorStatementScopesconvention: a namespace constant prefixed with yourgetType(), guarded by a unit test (copyEsStatementScopeTest). - TCCL. If your libraries reflect by class name or you spawn worker
threads, pin the plugin classloader at those boundaries (see the loci in
AGENTS.md;TcclPinningConnectorContextin fe-connector-paimon is a template). - Tests. Connector tests use recording fakes, not mocking frameworks
(example:
RecordingConnectorContext). Never touchconnector-metadata-methods.txtunless you changed the shared SPI surface itself. - Deployment-level settings (if any). A value that is one-per-FE rather
than one-per-catalog goes in your plugin's own settings file, NOT in
fe.conf: ship
src/main/resources/<name>.conf.template, add it to your assembly's<files>at the zip root, and read it withConnectorConf.get(context, "<key>", null, "<default>").<name>isConnectorProvider.name(), which is not necessarily your plugin directory name —plugins/connector/hive/holdshms.confandplugins/connector/trino/holdstrino-connector.conf. Guard that with a test assertingname() + ".conf.template"is on the classpath (copyIcebergConnectorConfTest#theConfTemplateIsNamedAfterTheProvider); a template under any other name deploys a file the engine never opens.build.shseeds the live.conffrom the template generically, so it needs no change. Do NOT add a key toConfig.javaor toDefaultConnectorContext.buildEnvironment— that is an engine change per setting, and the keys still there are only the ones several connectors share plus the fe.conf fallbacks kept for existing deployments. - Packaging. Add
src/main/assembly/plugin-zip.xml(copy from es or paimon). Verify your module throughpackage/install, not justtest-compile— shades and the plugin zip only materialize then. - Gates and e2e. Your module must pass the forbidden-import gate (runs
automatically; see
AGENTS.md). Add a docker environment underdocker/thirdparties/docker-compose/and regression suites underregression-test/suites/external_table_p0/regression-test/suites/external_table_p2.
Two named classes per connector, nothing else:
<Xxx>CatalogProperties— everything a user writes inCREATE CATALOG. A typed holder:@ConnectorPropertyfields bound byConnectorPropertiesUtils, plus derived read-only fields (enums, prefix maps, bounded numbers), plus the raw map, plus the key-name constants.of(map)binds, derives and validates in one step and throwsIllegalArgumentExceptionon a bad value, so an instance that exists has valid properties — which is what lets every reader downstream take a getter instead of parsing the map again.ConnectorProvider.validatePropertiesis then exactly one line,XxxCatalogProperties.of(properties), and that same line guards ALTER through the SPI defaultvalidatePropertiesForUpdate.<Xxx>Conf— the keys of the plugin's own<name>.conf(item 14 above), their defaults, their legacy fe.conf fallback keys, and small static readers wrappingConnectorConf.get. Only create it if the connector really has such settings.
Rules for of(map):
- No I/O and no heavyweight types. It runs at CREATE, at ALTER validation,
and on every connector rebuild — including on an FE replaying the edit log,
where reaching the filesystem would let a missing file stop FE from starting.
Remote and filesystem checks belong in
Connector#preCreateValidation, which runs on interactive CREATE only; note that ALTER does not run it, so those checks are deferred to first access after an ALTER. - Reject bad values, never unknown keys. The catalog property map also
carries engine keys (
type,meta.cache.*, ...) and storage keys (s3.*, ...), andALTER CATALOGmerges properties — it can overwrite a key but never remove one, so a rejected unknown key would leave a catalog that no statement can repair. - Required-ness is expressed by
ParamRules.requirealone. The annotation'srequiredattribute is not read by the connector binder; set it truthfully as documentation and mark optional fieldsrequired = falseexplicitly. Mind the polarity ofParamRules.check(condition, message): it throws when the condition holds, so the lambda states the failure case — the opposite of Guava'scheckArgument. Reversing it compiles cleanly and inverts the check. - Mark secrets
sensitive = trueand implementtoString()asConnectorPropertiesUtils.toMaskedString(this). That covers logs only; theSHOW CREATE CATALOGmasking is a separate, hand-maintained list in fe-core (DatasourcePrintableMap.SENSITIVE_KEY). - Never pass the raw map into anything that can reach user-visible output.
It holds the credentials.
ConnectorTableSchema's table properties are the trap:SHOW CREATE TABLErenders them unmasked, and the only thing keeping that rendering away from a given connector is that the connector does not declare theSUPPORTS_SHOW_CREATE_DDLcapability.
Everything else — session variable names, DDL statement keys, remote table-parameter keys, outbound SDK/BE payload keys, value literals — lives next to its single reader, not in a properties class.
Binding and annotation reflection must happen inside the plugin.
org.apache.doris.foundation. is child-first for the connector family (it is
parent-first for the filesystem family), so each plugin carries its own
@ConnectorProperty class, and fe-core reflecting on a plugin object would find
no annotations at all — silently, since getAnnotation just returns null.
Referencing another plugin's key therefore means copying the literal with a
comment naming the owner; IcebergSiblingProperties in the hive connector is
the precedent.
When migrating a connector that is already released, audit every numeric key
first. The binder throws on a malformed number, while the hand-written getInt
helpers it replaces silently fell back to the default — so a stored dirty value
becomes "catalog unusable until ALTER CATALOG overwrites it". List each such
key in the PR description and decide strict (the default) or lenient (bind as
String, convert inside of(), log a warning) explicitly.
fe-connector-adbc is the reference implementation of both classes.
- Unit tests live in each module; build/test recipes are in
AGENTS.md. - The shared SPI surface is guarded by fe-connector-spi's own suite
(
ConnectorMetadataSurfaceTestandConnectorPluginSurfaceTest). Whenever you touch fe-connector-spi, run that module's tests — a consumer-only test run will not catch a stale baseline. - Architecture gates run in the
validatephase of every FE build: the forbidden-import gate for this directory and the metadata-funnel gate for fe-core. Scripts and their self-tests live inbuild-support/andbuild-support/tests/. - End-to-end: docker environments under
docker/thirdparties/docker-compose/, suites underregression-test/suites/external_table_p0andexternal_table_p2.
Update this file ONLY when a framework-level fact changes:
- a module is added, removed, or renamed under
fe/fe-connector/; - the loading / lifecycle model changes (provider discovery, the per-statement metadata funnel, classloading, packaging);
- a durable invariant appears that no build gate or test can express.
Do NOT update this file for SPI method changes (javadoc is the API
reference; connector-metadata-methods.txt is the recorded surface), new
capability flags, or bug fixes.