mirror of
https://github.com/apache/doris.git
synced 2026-11-04 07:31:17 +00:00
### 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 - [x] Regression test - [x] 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. - [x] 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. - [x] Yes. Doc PR to follow: connector plugin layout, `connector_plugin_root`, the `trino_connector_plugin_dir` default, and the `lakesoul` removal. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
5.7 KiB
Bash
Executable File
127 lines
5.7 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
# Arch gate: the per-statement ConnectorMetadata funnel.
|
|
#
|
|
# Invariant: within a statement, fe-core must reuse exactly ONE ConnectorMetadata
|
|
# instance per catalog. Every read / scan / DDL / MVCC seam therefore acquires its
|
|
# metadata through the engine-side funnel
|
|
# org.apache.doris.datasource.plugin.PluginDrivenMetadata.get(session, connector)
|
|
# which memoizes connector.getMetadata(session) on the statement's ConnectorStatementScope
|
|
# and closes it deterministically at statement end. A bare connector.getMetadata(session)
|
|
# anywhere else in fe-core silently mints a second, unmanaged instance and breaks the
|
|
# invariant with no compile error and no test failure — this gate fails the build on it.
|
|
#
|
|
# Allowed (kept silent):
|
|
# 1. PluginDrivenMetadata.java — the funnel itself, the ONE legal direct call site
|
|
# (its javadoc mentions of the call live here too).
|
|
# 2. Any bare call whose line — or the line immediately above it — carries the marker
|
|
# "getMetadata-funnel-exempt": the write-path seams (INSERT / DELETE / MERGE sink,
|
|
# bind, row-level DML), temporarily exempt until the write-sharing step reroutes them
|
|
# through the funnel. Deleting a marker there auto-tightens the gate onto that site.
|
|
# 3. A no-argument getMetadata() call (RuntimeProfile's node probe, TestExternalCatalog's
|
|
# static map): a different method, not the Connector#getMetadata(ConnectorSession) SPI.
|
|
# 4. A comment line that merely names the call — never executable.
|
|
#
|
|
# The match is anchored to the call form (".getMetadata(" with an argument), NOT to
|
|
# getMetadataTableRows(...) or the API declaration.
|
|
#
|
|
# Self-test: build-support/tests/test-fe-core-metadata-funnel.sh.
|
|
#
|
|
# Usage:
|
|
# build-support/check-fe-core-metadata-funnel.sh # default root fe/fe-core
|
|
# build-support/check-fe-core-metadata-funnel.sh <fe-core-dir> # supplied root
|
|
#
|
|
# Exit code:
|
|
# 0 — no un-exempt bare calls
|
|
# 1 — at least one found (offending lines printed)
|
|
# 2 — search root not found
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
DEFAULT_ROOT="${SCRIPT_DIR}/../fe/fe-core"
|
|
ROOT="${1:-${DEFAULT_ROOT}}"
|
|
|
|
if [ ! -d "${ROOT}" ]; then
|
|
echo "check-fe-core-metadata-funnel: search root not found: ${ROOT}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# The funnel carrier — the ONE file allowed to call Connector#getMetadata directly.
|
|
FUNNEL_REL='datasource/plugin/PluginDrivenMetadata.java'
|
|
|
|
# Inline marker for the write-path seams still bypassing the funnel (removed by the write-sharing step).
|
|
# It may sit on the call line or on the line directly above it (long call lines keep the marker above).
|
|
MARKER='getMetadata-funnel-exempt'
|
|
|
|
# An invocation of getMetadata with an argument. Two forms so a call whose argument is wrapped onto the
|
|
# next line (open paren at end of line) is still caught, not just the single-line form:
|
|
ARG_SAMELINE='\.getMetadata\([[:space:]]*[^[:space:])]' # ".getMetadata(session" — arg on this line
|
|
ARG_WRAPPED='\.getMetadata\([[:space:]]*$' # ".getMetadata(" — arg on next line
|
|
|
|
# Candidate lines: any ".getMetadata(" under main sources. Narrowed to real arg calls in the loop.
|
|
CANDIDATES=$(grep -rEn '\.getMetadata\(' "${ROOT}/src/main/java" 2>/dev/null || true)
|
|
|
|
RESULT=""
|
|
if [ -n "${CANDIDATES}" ]; then
|
|
while IFS= read -r line; do
|
|
[ -z "${line}" ] && continue
|
|
# line = <file>:<lineno>:<code>
|
|
file="${line%%:*}"
|
|
rest="${line#*:}"
|
|
lineno="${rest%%:*}"
|
|
code="${rest#*:}"
|
|
|
|
# (4) comment line merely naming the call — never executable.
|
|
trimmed="${code#"${code%%[![:space:]]*}"}"
|
|
case "${trimmed}" in '*'*|'//'*|'/*'*) continue ;; esac
|
|
|
|
# (3) no-argument getMetadata() — a different method, not the SPI call.
|
|
if ! { [[ "${code}" =~ ${ARG_SAMELINE} ]] || [[ "${code}" =~ ${ARG_WRAPPED} ]]; }; then
|
|
continue
|
|
fi
|
|
|
|
# (1) the funnel file itself.
|
|
case "${file}" in *"${FUNNEL_REL}") continue ;; esac
|
|
|
|
# (2) tracked write-path exemption marker, on the call line ...
|
|
case "${code}" in *"${MARKER}"*) continue ;; esac
|
|
# ... or on the line immediately above it.
|
|
if [ "${lineno}" -gt 1 ]; then
|
|
prev="$(sed -n "$((lineno - 1))p" "${file}" 2>/dev/null || true)"
|
|
case "${prev}" in *"${MARKER}"*) continue ;; esac
|
|
fi
|
|
|
|
RESULT="${RESULT}${line}"$'\n'
|
|
done <<< "${CANDIDATES}"
|
|
fi
|
|
RESULT=$(printf '%s' "${RESULT}" | sed '/^$/d')
|
|
|
|
if [ -n "${RESULT}" ]; then
|
|
echo "BARE Connector#getMetadata() calls in fe-core (must route through PluginDrivenMetadata.get):" >&2
|
|
echo "${RESULT}" >&2
|
|
echo "" >&2
|
|
echo "A statement must reuse ONE ConnectorMetadata per catalog. Acquire it via" >&2
|
|
echo "PluginDrivenMetadata.get(session, connector), not connector.getMetadata(session)." >&2
|
|
echo "Write-path seams pending the write-sharing step carry a '${MARKER}' marker" >&2
|
|
echo "(on the call line or the line directly above it)." >&2
|
|
exit 1
|
|
fi
|