[DSIP-110][Datasource] Support Custom Datasource Types via External Plugins
Search before asking
- I had searched in the DSIP and found no similar DSIP.
Functional requirement origin: #18377 "[Improvement][datasource] Enable users to customize datasources".
Motivation
DolphinScheduler's datasource layer is pluginized in name but not in fact. Although datasource support is implemented as SPI plugins (DataSourceProcessor / DataSourceChannelFactory, 28 built-in plugins), the plugin contract is hard-wired to the compiled-in DbType enum (dolphinscheduler-spi/.../spi/enums/DbType.java):
DataSourceProcessor#getDbType()returnsDbType, a compile-time enum shipped insidedolphinscheduler-spi. A plugin compiled outside the DolphinScheduler tree can only return an existing constant — it can override a built-in type, but can never introduce a new one.BaseDataSourceParamDTO#getType()returnsDbTypeas well, so the parameter DTOs have the same constraint.- The
typecolumn oft_ds_datasourcestores the enum's integer code (tinyint), which by definition cannot represent an unknown type. - Both frontend surfaces — the "create datasource" dialog (
dolphinscheduler-ui/src/views/datasource/list/use-form.ts) and the task-node datasource selector (.../node/fields/use-datasource.ts) — enumerate types from hardcoded arrays; a type installed on the backend is invisible to users.
As a result, anyone whose database is not among the built-in types (proprietary engines, internal database proxies, newly released databases) has no supported extension path short of forking the project, editing the enum, the DAO layer and the frontend, and rebuilding everything.
Goal (functional requirement) — a deployment administrator can install an externally compiled datasource plugin package into an existing DolphinScheduler installation, without modifying or recompiling any DolphinScheduler code, and the new datasource type becomes usable end-to-end:
- FR1 — Registerable: the plugin can declare a datasource type identity that is unique and resolvable across api-server, master and worker.
- FR2 — Visible: the new type shows up in the UI, both in the datasource creation entry and in the datasource selector of task nodes that consume datasources.
- FR3 — Full lifecycle: for the new type, users can create a datasource instance, test connectivity, update it, list/browse it, grant it to other users, and (where the driver supports it) browse metadata (databases / tables / columns).
- FR4 — Executable: tasks that reference datasource instances of the new type run correctly (minimum scope: SQL and Procedure tasks).
- FR5 — Install-only rollout: installation = drop the plugin package into the plugins directory and restart the servers; no source change, no rebuild of DolphinScheduler itself.
Non-goals (proposed for v1):
- No plugin hot-loading / installing plugins through the web UI. Plugin installation stays an administrator/deployment action (see Security alignment below).
- No plugin-provided custom UI forms. Custom types get a generic JDBC-style form (see open decision D3).
- No DataX / Sqoop support for custom types in v1 (their type→generator mappings are hardcoded; see Aspect 5).
Design Detail
The requirement decomposes into the following aspects. Each aspect states the current code fact that blocks the requirement, and what has to be true after the change.
Aspect 0 — Current state summary (why each aspect exists)
| # | Aspect | Blocking fact today |
|---|---|---|
| 1 | Type identity in the SPI contract | getDbType() / getType() return compiled-in DbType; registry keyed by enum name |
| 2 | Type discovery | No API exposes registered types; frontend hardcodes arrays |
| 3 | Persistence | t_ds_datasource.type is tinyint storing the enum code; entity field is DbType |
| 4 | api-server handling | Routing and dialect behavior switch on DbType (DbType.ofName, valueOf, switch statements) |
| 5 | Task execution path | SqlTask/ProcedureTask/DataxTask do DbType.valueOf(...); context objects carry DbType |
| 6 | Frontend | Type lists, per-type forms and default ports are hardcoded in use-form.ts / use-datasource.ts / types.ts |
| 7 | Packaging & driver delivery | Works today (shaded jars on the classpath via plugins/datasource-plugins), needs conventions for external plugins |
| 8 | Security alignment | Plugin install must stay inside the existing trusted-deployment boundary |
Aspect 1 — String-based type identity in the plugin contract
The SPI must stop using a compile-time enum as the identity of a datasource type.
- A datasource type is identified by a unique, case-normalized string name (e.g.
"MYSQL","DORIS","MY_INTERNAL_DB"), declared by the plugin and used consistently byDataSourceProcessor,DataSourceChannelFactoryand the parameter DTOs. DbTypedegrades from "the closed set of all possible types" to "named constants for the built-in types" (see open decision D1). All identity lookups (DataSourcePluginManagermaps,DataSourceUtilsrouting) become string-keyed; today they already key maps bydbType.getName(), so the direction of change is natural.- Conflict rules: two installed plugins declaring the same type name must fail fast at startup with a clear message. The existing
PrioritySPImechanism (same name resolved by priority, ambiguity raises) already provides the semantics for deliberately overriding a built-in type; new names simply must not collide. BaseDataSourceParamDTOcarries the type as a string field, replacinggetType(): DbType, so JSON round-tripping works for types unknown to the core.
Aspect 2 — Type registry and a discovery API
The backend must be able to enumerate what is installed, and the frontend must learn the type list from the backend instead of hardcoding it.
DataSourcePluginManager(or a thin façade over it) exposes the registered types together with minimal metadata a UI needs: unique name, display label, and simple capability flags (e.g.supportsConnectivityTest,supportsMetadata,jdbcCompatible).- A new REST endpoint, e.g.
GET /datasources/types, returns that list. This is the single contract between backend plugins and both frontend surfaces (Aspect 6). - Create/update APIs validate the submitted type against the registry and reject unknown types with an explicit error message ("datasource type X is not installed") instead of failing deep inside enum parsing (
DbType.ofNamecurrently throwsNoSuchElementExceptionfor unknown names). - Capability flags replace enum switches where behavior varies per type (see Aspect 4), so a custom type gets sane defaults without core code knowing it.
Aspect 3 — Persistence of the datasource type
t_ds_datasource.type must be able to store identities that do not exist at compile time.
- Change the column from
tinyint(integer enum code) tovarcharstoring the type name string, in all three fresh-install schemas (MySQL, PostgreSQL, H2:dolphinscheduler-dao/src/main/resources/sql/dolphinscheduler_{mysql,postgresql,h2}.sql) and keep theUNIQUE (name, type)constraint semantics. - The
DataSourceentity (dolphinscheduler-dao/.../entity/DataSource.java) changes itstypefield fromDbTypetoString; mappers and repositories follow. - Upgrade scripts under
dolphinscheduler-dao/src/main/resources/sql/upgrade/<version>_schema/{mysql,postgresql}/convert existing integer codes to name strings (a code→name mapping over the current 29 enum values; see Migration Plan). The stale column comment ("0:mysql,1:postgresql,...") is corrected at the same time. - Graceful degradation: a datasource row whose type plugin is not installed must still be listable (the UI shows the raw type name and marks it unavailable); connect/run attempts return a clear "plugin not found" error rather than an NPE or enum parse failure. The message
"datasource plugin '%s' is not found"already exists inDataSourceClientProviderand should become the uniform behavior.
Aspect 4 — api-server adaptations
- Routing:
DataSourceUtils.buildDatasourceParamroutes the submitted JSON to the right processor by string type name (today:DbType.ofName(type.toUpperCase())). - API surface:
DataSourceController#queryDataSourceListbindstypeasDbType(Spring enum-name conversion). Binding it asStringkeeps the wire format ("MYSQL") unchanged — the frontend already sends enum names, so this is not a breaking API change. - Dialect behavior: type-specific switches such as
DataSourceServiceImpl#getDbSchemaPattern(HIVE/ORACLE/SQLSERVER/CLICKHOUSE/DATABEND/PRESTO) and theAbstractDataSourceProcessorREDSHIFT special case must become capability/metadata-driven (Aspect 2) with safe defaults, so unknown types get working default behavior for metadata browsing instead of being silently wrong. - Unchanged by design: password encryption (
PasswordUtils), Kerberos handling, and datasource authorization are already type-agnostic (permissions bind to datasource ids).
Aspect 5 — Task execution path (master + worker)
Good news first: task definitions already store the datasource type as a string in task_params JSON (SqlParameters.type, DataxParameters.dsType/dtType are String), and datasource instances are referenced by integer id. The work is removing enum round-trips:
- master:
TaskExecutionContextFactory#assembleDataSourceParametersputs aDbTypeinto the execution context; context objects (DataxTaskExecutionContext,SqoopTaskExecutionContext, ...) carryDbTypefields — all become strings. - worker task plugins:
SqlTask(DbType.valueOf(sqlParameters.getType())),ProcedureTask, and the DataX/Sqoop call sites resolve the processor by string type name via the plugin manager. Unknown type → clear task failure message naming the missing plugin. - DataX / Sqoop scope:
DataxUtils#getReaderPluginName/getWriterPluginNameand the Sqoop source/target generators hardcode type→external-tool mappings that only make sense for built-in types. In v1 these tasks declare themselves limited to built-in types (the frontend already supports a per-task-type whitelist viasupportedDatasourceType), so custom types are selectable only for SQL / Procedure tasks. Lifting this restriction is a follow-up, not part of this DSIP.
Aspect 6 — Frontend: dynamic type list + generic fallback form
- Type list source: both hardcoded arrays —
datasourceTypeListinviews/datasource/list/use-form.ts(28 entries with per-type labels/default ports) anddatasourceTypesinnode/fields/use-datasource.ts(an inconsistent subset, some entries disabled) — are replaced by data fromGET /datasources/types(Aspect 2). Per-task-type whitelisting (supportedDatasourceType) continues to filter the dynamic list. - Forms for custom types: built-in types keep their dedicated forms (
changeTypeinuse-form.ts). A type the frontend has no dedicated form for renders a generic JDBC form: host, port, user, password, database, extra JDBC parameters (other), optional principal — the fields the commonBaseConnectionParamalready supports. This is sufficient for the overwhelming majority of JDBC drivers; the display label and default port come from the type metadata. - Type definitions: the
IDataBasestring-literal union inservice/modules/data-source/types.tsbecomes a plainstringvalidated at runtime against the types API. - i18n / labels: labels for built-in types keep current texts; custom types display the label from the API metadata (no per-type i18n keys needed for custom types).
Aspect 7 — Packaging, delivery and driver management
The deployment path already works mechanically (shaded plugin jars in plugins/datasource-plugins/ are appended to the classpath by the server start scripts, discovered via ServiceLoader); what this DSIP adds is convention and guarantees:
- A custom plugin is a single shaded jar bundling: the
DataSourceProcessor+DataSourceChannelFactoryimplementations,META-INF/servicesregistrations (e.g. via@AutoService), and the JDBC driver it needs (same pattern as built-in plugin poms, documented indocs/docs/en/contribute/backend/spi/datasource.md). - The plugin must be installed on every api-server, master and worker node that will touch the type (api for UI/lifecycle, master+worker for execution); the developer/operations documentation states this explicitly.
- Driver version conflicts between plugins remain the known limitation of the flat-classpath model; document it as a constraint (one driver major version per classpath), same as today's behavior for built-in plugins. Classloader isolation is explicitly out of scope.
- Reference plugin:
DbTypecontainsH2(9)but no H2 plugin module exists. Implementing the H2 datasource plugin as the first "externally developed" plugin gives the proposal a living example that is testable in CI with zero external services (in-memory JDBC), and doubles as the test vehicle for the whole path (registry → UI → SQL task execution).
Aspect 8 — Security alignment
This feature must stay within the trust boundaries documented in the project's security model:
- Installing a plugin jar is an administrator/deployment action (filesystem access to the server), identical in trust level to installing task plugins or storage plugins today. Ordinary platform users cannot install plugins; they can only use an installed type within existing datasource authorization. The feature adds no new privilege escalation path.
- Parameters a user enters into a custom datasource (including JDBC
otherparameters) fall squarely under the model's existing "user-configured plugin parameters are trusted user behavior" clause. - Registry conflict handling (Aspect 1) must fail fast and loudly, so a malicious/accidental same-name plugin cannot silently override a built-in type without priority rules catching it.
Open design decisions (for the mail-thread discussion)
- D1 — Fate of
DbType: (a) recommended: keep the enum as a deprecated constants holder for migration/compat, remove its integer code from persistence, all runtime paths use strings — mirrors how task plugins useString taskType; (b) keep full enum semantics for built-ins and add a parallel string path — rejected as dual-maintenance. - D2 — Storage: (a) recommended: in-place
typecolumn conversiontinyint → varchar(64)with data migration; (b) side tabletype_code → type_namemapping — rejected (extra join, worse ergonomics, still can't express custom types in the main column). - D3 — UI for custom types: (a) recommended for v1: generic JDBC fallback form; (b) plugin-declared form schema returned by the types API (field list rendered dynamically) — more powerful, deferred to a follow-up DSIP.
- D4 — Task coverage in v1: SQL + Procedure only; DataX/Sqoop restricted to built-ins via existing whitelist mechanism.
- D5 — Types API shape:
GET /datasources/typesreturning[{ type, label, defaultPort?, capabilities... }]— exact field list to be settled in review.
Suggested sub-task breakdown
Consistent with the DSIP process, this is large enough to be split:
- SPI contract: string type identity in
DataSourceProcessor/ DTOs /DataSourcePluginManager(+ conflict rules). - DAO:
DataSourceentity, mappers, fresh-install schemas for MySQL/PG/H2. - Upgrade scripts (code→name migration, MySQL + PostgreSQL) and tools support.
- api-server: string routing,
GET /datasources/types, unknown-type validation, capability-driven dialect defaults. - master/worker: execution context and SQL/Procedure/DataX/Sqoop call-site adaptation.
- Frontend: dynamic type lists, generic fallback form, whitelist filtering.
- Reference H2 plugin + developer guide (
contribute/backend/spi/datasource.mden/zh) + ops doc (install on all nodes, driver conflicts). - Tests & e2e (see Test Plan).
Compatibility, Deprecation, and Migration Plan
Data migration
t_ds_datasource.type:tinyint/int→varchar, one upgrade script per dialect (MySQL, PostgreSQL) in the upgrade directory of the release carrying this change, following the existingt_ds_version-drivenUpgradeDaoflow. The DML maps each of the 29 live integer codes to its name constant (0→MYSQL, 1→POSTGRESQL, ..., 28→DOLPHINDB). Rows referencing codes with no enum value (corrupted data) are reported rather than silently converted.task_paramsint_ds_task_definition/t_ds_task_instancealready stores type names as strings and is not touched.connection_paramsJSON is plugin-owned and unchanged.
API compatibility
- Wire formats that already use enum names (e.g.
GET /datasources/list?type=MYSQL) remain byte-compatible; only server-side binding types change. - Java-level breakage is confined to internal modules (
dolphinscheduler-spi,dolphinscheduler-datasource-api,dolphinscheduler-dao) — this is a dev-branch feature release, but downstream plugin authors are affected and must be informed via migration notes: existing third-party plugins compiled againstgetDbType()need a trivial recompile against the new contract.
Deployment / rolling upgrade
- All servers (api, master, worker) must be upgraded together in a cluster that uses custom types; a node missing a plugin degrades gracefully (Aspect 3) — datasource lists still render, connect/run fail with an explicit "plugin not installed" error.
- Built-in-only deployments keep working identically after migration; no user action required.
Deprecations
DbTypeinteger codes and@EnumValuepersistence deprecated; enum retained (D1) as constants for one removal cycle.
Test Plan
Unit tests
DataSourcePluginManager: string-keyed registration, duplicate-name fail-fast, priority override, missing-plugin lookup error.DataSourceUtils/ processor routing by string type; unknown type rejected with explicit error.- Migration converter: every one of the 29 codes maps to the expected name; unknown code surfaces an error.
- Controller binding:
typeaccepts any registered name; unregistered names produce a 4xx with a clear message.
Integration / DAO tests
- Upgrade test from the previous schema with rows covering (at least) MYSQL, POSTGRESQL, HIVE, ORACLE, SSH, DOLPHINDB — assert post-migration names and uniqueness.
- api-server: create / update / connect-test / list / authorize a datasource of a custom test processor; datasource rows with an uninstalled type list correctly and fail connect with the documented error.
Worker / execution tests
SqlTaskandProcedureTaskagainst the reference H2 plugin (in-memory): full create → bind → execute path with a custom type.- Failure-path test: task referencing an uninstalled type fails with the "plugin not found" message.
Frontend / e2e
dolphinscheduler-e2e: create an H2 datasource through the generic fallback form, bind it to an SQL task node, run the workflow successfully.- Regression: existing datasource e2e suites (mysql, postgresql, hive, clickhouse, sqlserver, dolphindb docker fixtures) pass unchanged.
Regression scope
- All 28 built-in plugins' existing processor/channel unit tests pass unmodified beyond the contract recompile.
- Built-in UI forms render exactly as before (no visual/behavioral change for known types).
Code of Conduct
- I agree to follow this project's Code of Conduct
Source: apache/dolphinscheduler