Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions build-support/check-fe-core-jdbc-free.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/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
#
# Arch gate: fe-core carries no JDBC data-source implementation.
#
# Invariant: every JDBC dialect client, type mapping, driver-jar policy and connection pool lives in
# fe/fe-connector (the jdbc plugin and the shared spi policy), never in fe-core. fe-core reaches a JDBC
# source only through the connector SPI (PluginDrivenExternalCatalog for catalogs, StreamingSourceClient
# for streaming jobs). A dialect fix that lands in fe-core again would silently fork the implementation
# — the exact drift this gate exists to prevent — with no compile error and no test failure.
#
# Forbidden in fe-core main sources:
# 1. import org.apache.doris.datasource.jdbc.* — the deleted legacy client package; it must not return.
# 2. import com.zaxxer.hikari.* — a connection pool is a connector concern.
# 3. import java.sql.<connection or statement type> — Connection, DriverManager, Driver, Statement,
# PreparedStatement, ResultSet, ResultSetMetaData, DatabaseMetaData, DataSource, SQLException.
# The value types (java.sql.Timestamp/Date/Time/Types) are not JDBC access and stay allowed.
#
# Exempt (kept silent):
# - org/apache/doris/httpv2/** — the FE's HTTP SQL gateway, which talks to the FE ITSELF over the
# MySQL protocol through a JDBC driver. That is not an external data source.
# - A comment line that merely names an import — never executable.
#
# Self-test: build-support/tests/test-fe-core-jdbc-free.sh.
#
# Usage:
# build-support/check-fe-core-jdbc-free.sh # default root fe/fe-core
# build-support/check-fe-core-jdbc-free.sh <fe-core-dir> # supplied root
#
# Exit code:
# 0 — no forbidden imports
# 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-jdbc-free: search root not found: ${ROOT}" >&2
exit 2
fi

# The FE's own HTTP SQL gateway: JDBC to the FE itself, exempt by path.
EXEMPT_DIR='org/apache/doris/httpv2/'

# The three forbidden import shapes (anchored to an import statement, so a comment or a string that
# names the package is not matched).
LEGACY_PKG='^[[:space:]]*import[[:space:]]+(static[[:space:]]+)?org\.apache\.doris\.datasource\.jdbc\.'
POOL='^[[:space:]]*import[[:space:]]+(static[[:space:]]+)?com\.zaxxer\.hikari\.'
JDBC_ACCESS='^[[:space:]]*import[[:space:]]+(static[[:space:]]+)?java\.sql\.(Connection|DriverManager|Driver|Statement|PreparedStatement|CallableStatement|ResultSet|ResultSetMetaData|DatabaseMetaData|DataSource|SQLException)([[:space:].;]|$)'

CANDIDATES=$(grep -rEn "${LEGACY_PKG}|${POOL}|${JDBC_ACCESS}" "${ROOT}/src/main/java" 2>/dev/null || true)

RESULT=""
if [ -n "${CANDIDATES}" ]; then
while IFS= read -r line; do
[ -z "${line}" ] && continue
file="${line%%:*}"
case "${file}" in *"${EXEMPT_DIR}"*) continue ;; esac
RESULT="${RESULT}${line}"$'\n'
done <<< "${CANDIDATES}"
fi
RESULT=$(printf '%s' "${RESULT}" | sed '/^$/d')

if [ -n "${RESULT}" ]; then
echo "JDBC data-source access in fe-core (must live in fe/fe-connector and be reached through the SPI):" >&2
echo "${RESULT}" >&2
echo "" >&2
echo "fe-core must not import org.apache.doris.datasource.jdbc.*, com.zaxxer.hikari.* or the" >&2
echo "java.sql connection/statement types. A JDBC source is reached through the connector SPI" >&2
echo "(PluginDrivenExternalCatalog / StreamingSourceClient); dialect logic belongs in" >&2
echo "fe/fe-connector/fe-connector-jdbc. Only ${EXEMPT_DIR} (JDBC to the FE itself) is exempt." >&2
exit 1
fi
128 changes: 128 additions & 0 deletions build-support/tests/test-fe-core-jdbc-free.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/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
#
# Self-test for build-support/check-fe-core-jdbc-free.sh.
#
# The gate exits 0 on the real (already-clean) tree, so a controlled RED/GREEN fixture is the only way
# to prove it catches what it must and to lock the behavior against silent regression.
# Each seeded case targets one gate property:
# RED — import of the deleted legacy client package (the core violation)
# RED — import of the HikariCP pool (pool is a connector concern)
# RED — import of java.sql.Connection / a static import of DriverManager.getConnection
# SILENT — java.sql.Timestamp / java.sql.Types (value types, not JDBC access)
# SILENT — the same forbidden imports under httpv2/ (JDBC to the FE itself, exempt by path)
# SILENT — a comment or string that names a forbidden package (never an import)
# SILENT — a class whose simple name merely starts with a forbidden one (java.sql.Statement vs
# a hypothetical java.sql.StatementEvent is matched on the whole simple name)
# Plus: exit 0 on a clean tree.
#
# Usage: bash build-support/tests/test-fe-core-jdbc-free.sh # exit 0 = pass, 1 = fail

set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GATE="${SCRIPT_DIR}/../check-fe-core-jdbc-free.sh"

FX="$(mktemp -d)"
trap 'rm -rf "${FX}"' EXIT

SRC="${FX}/src/main/java/org/apache/doris"
mkdir -p "${SRC}/job" "${SRC}/httpv2/util" "${SRC}/common"

# RED: the legacy package, the pool, and a JDBC connection type.
cat > "${SRC}/job/LegacyClientUser.java" <<'EOF2'
package org.apache.doris.job;
import org.apache.doris.datasource.jdbc.client.JdbcClient;
public class LegacyClientUser {
}
EOF2

cat > "${SRC}/job/PoolUser.java" <<'EOF2'
package org.apache.doris.job;
import com.zaxxer.hikari.HikariDataSource;
public class PoolUser {
}
EOF2

cat > "${SRC}/job/ConnectionUser.java" <<'EOF2'
package org.apache.doris.job;
import java.sql.Connection;
import static java.sql.DriverManager.getConnection;
public class ConnectionUser {
}
EOF2

# SILENT: value types, comments and strings, and a longer simple name.
cat > "${SRC}/common/ValueTypes.java" <<'EOF2'
package org.apache.doris.common;
import java.sql.Timestamp;
import java.sql.Types;
import java.sql.StatementEvent;
public class ValueTypes {
// import java.sql.Connection; is only mentioned here
String s = "import org.apache.doris.datasource.jdbc.client.JdbcClient";
}
EOF2

# SILENT: the FE's own HTTP SQL gateway is exempt by path.
cat > "${SRC}/httpv2/util/StatementSubmitter.java" <<'EOF2'
package org.apache.doris.httpv2.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
public class StatementSubmitter {
}
EOF2

FAILED=0
fail() { echo "FAIL: $1"; FAILED=1; }

# ---- run 1: mixed fixture -> exactly the RED imports flagged ----
OUT="$(bash "${GATE}" "${FX}" 2>&1)"; EC=$?
REPORTED="$(printf '%s\n' "${OUT}" | grep -E "^${FX}.*:[0-9]+:" || true)"
N="$(printf '%s\n' "${REPORTED}" | grep -c 'import' || true)"

[ "${EC}" -eq 1 ] || fail "expected exit 1 (violations present), got ${EC}"
[ "${N}" -eq 4 ] || fail "expected exactly 4 reported violations, got ${N}"$'\n'"${REPORTED}"

must_report() { printf '%s\n' "${REPORTED}" | grep -qF "$1" || fail "violation NOT reported: $1"; }
must_report 'job/LegacyClientUser.java:'
must_report 'job/PoolUser.java:'
must_report 'import java.sql.Connection;'
must_report 'import static java.sql.DriverManager.getConnection;'

must_not_report() {
printf '%s\n' "${REPORTED}" | grep -qF "$1" && fail "should NOT be reported: $1" || true
}
must_not_report 'common/ValueTypes.java:'
must_not_report 'httpv2/util/StatementSubmitter.java:'

# ---- run 2: remove the RED cases -> clean tree exits 0 ----
rm -f "${SRC}/job/LegacyClientUser.java" "${SRC}/job/PoolUser.java" "${SRC}/job/ConnectionUser.java"
bash "${GATE}" "${FX}" >/dev/null 2>&1; EC=$?
[ "${EC}" -eq 0 ] || fail "expected exit 0 on the clean fixture, got ${EC}"

# ---- run 3: a missing root is reported as such, not as clean ----
bash "${GATE}" "${FX}/does-not-exist" >/dev/null 2>&1; EC=$?
[ "${EC}" -eq 2 ] || fail "expected exit 2 for a missing search root, got ${EC}"

if [ "${FAILED}" -ne 0 ]; then
echo "test-fe-core-jdbc-free: FAILED"
exit 1
fi
echo "test-fe-core-jdbc-free: OK"
10 changes: 9 additions & 1 deletion fe/fe-connector/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ mvn -f fe/pom.xml -pl :fe-connector-spi -am test \

## Machine-Checked Obligations

Two architecture gates run in the `validate` phase (scripts and their
Three architecture gates run in the `validate` phase (scripts and their
self-tests live in `build-support/` and `build-support/tests/`):

1. **Forbidden imports** — `build-support/check-fe-connector-imports.sh`,
Expand All @@ -46,6 +46,14 @@ self-tests live in `build-support/` and `build-support/tests/`):
`PluginDrivenMetadata` may call `Connector#getMetadata`; exempt call
sites carry a `getMetadata-funnel-exempt` marker, and deleting a marker
auto-tightens the gate.
3. **No JDBC access in fe-core** — `build-support/check-fe-core-jdbc-free.sh`,
wired into fe-core's `pom.xml`. fe-core main sources must not import
`org.apache.doris.datasource.jdbc.*`, `com.zaxxer.hikari.*` or the
`java.sql` connection/statement types; a JDBC source is reached through
the SPI (`PluginDrivenExternalCatalog`, `StreamingSourceClient`), and
dialect logic belongs in `fe-connector-jdbc`. The driver-jar policy every
connector applies is `DriverUrlPolicy` in fe-connector-spi. Only
`httpv2/` (JDBC to the FE itself) is exempt.

**Changing the shared SPI surface (fe-connector-spi):** regenerate BOTH
recorded baselines in the SAME commit — `connector-metadata-methods.txt`
Expand Down
6 changes: 3 additions & 3 deletions fe/fe-connector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,9 @@ String, convert inside `of()`, log a warning) explicitly.
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 `validate` phase 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 in `build-support/` and
`build-support/tests/`.
forbidden-import gate for this directory, and the metadata-funnel and
JDBC-free gates for fe-core. Scripts and their self-tests live in
`build-support/` and `build-support/tests/`.
- **End-to-end**: docker environments under
`docker/thirdparties/docker-compose/`, suites under
`regression-test/suites/external_table_p0` and `external_table_p2`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
* file, and a URL each node downloads for itself cannot promise that. Rejecting remote schemes outright
* keeps that failure -- which surfaces as an unreadable partition, far from its cause -- unreachable.
*
* <p>This does NOT reuse {@code ConnectorValidationContext#validateAndResolveDriverPath}: that one resolves
* <p>This does NOT reuse the shared {@code DriverUrlPolicy} of fe-connector-spi: that one resolves
* against {@code jdbc_drivers_dir} and enforces a {@code .jar} grammar.
*/
public final class AdbcDriverPathResolver {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.apache.doris.connector.spi.ConnectorTestResult;
import org.apache.doris.connector.spi.ConnectorValidationContext;
import org.apache.doris.connector.spi.DorisConnectorException;
import org.apache.doris.connector.spi.DriverUrlPolicy;
import org.apache.doris.connector.spi.handle.ConnectorTableHandle;
import org.apache.doris.connector.spi.mvcc.ConnectorMvccPartitionView;
import org.apache.doris.connector.spi.procedure.ConnectorProcedureOps;
Expand Down Expand Up @@ -1549,9 +1550,9 @@ static boolean pinPoolThreadsToClassLoader(ExecutorService pool, int poolSize, C

/**
* Enforces JDBC driver-url security at CREATE CATALOG (mirrors {@code PaimonConnector.preCreateValidation}):
* for the jdbc flavor a configured {@code iceberg.jdbc.driver_url} is routed through the engine's
* {@link ConnectorValidationContext#validateAndResolveDriverPath} hook (the FE format /
* {@code jdbc_driver_url_white_list} / {@code jdbc_driver_secure_path} gates), so a rejected url fails
* for the jdbc flavor a configured {@code iceberg.jdbc.driver_url} is checked against the FE's shared
* driver-jar policy ({@link DriverUrlPolicy}: format / {@code jdbc_driver_url_white_list} /
* {@code jdbc_driver_secure_path}, fed from the engine environment), so a rejected url fails
* CREATE CATALOG before the jar is ever loaded by {@link #maybeRegisterJdbcDriver}. Non-jdbc flavors are
* a no-op.
*/
Expand All @@ -1562,7 +1563,8 @@ public void preCreateValidation(ConnectorValidationContext validationContext) th
}
String driverUrl = IcebergJdbcMetaStoreProperties.of(properties).getDriverUrl();
if (StringUtils.isNotBlank(driverUrl)) {
validationContext.validateAndResolveDriverPath(driverUrl);
DriverUrlPolicy.resolve(driverUrl,
DriverUrlPolicy.Settings.fromContext(context, configuredDriversDir()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.doris.connector.spi.ConnectorColumn;
import org.apache.doris.connector.spi.ConnectorMetadata;
import org.apache.doris.connector.spi.ConnectorPassthroughSqlOps;
import org.apache.doris.connector.spi.ConnectorQueryResult;
import org.apache.doris.connector.spi.ConnectorSession;
import org.apache.doris.connector.spi.ConnectorStatementScopes;
import org.apache.doris.connector.spi.ConnectorTableSchema;
Expand Down Expand Up @@ -145,6 +146,15 @@ public ConnectorTableSchema getTableSchema(
return new ConnectorTableSchema(tableName, columns, "JDBC", props.getRaw());
}

@Override
public List<String> getPrimaryKeys(ConnectorSession session, ConnectorTableHandle handle) {
if (handle instanceof PassthroughQueryTableHandle) {
return Collections.emptyList();
}
JdbcTableHandle jdbcHandle = (JdbcTableHandle) handle;
return client.getPrimaryKeys(jdbcHandle.getRemoteDbName(), jdbcHandle.getRemoteTableName());
}

@Override
public Optional<ConnectorTableStatistics> getTableStatistics(
ConnectorSession session, ConnectorTableHandle handle) {
Expand Down Expand Up @@ -250,6 +260,11 @@ public void executeStmt(ConnectorSession session, String stmt) {
client.executeStmt(stmt);
}

@Override
public ConnectorQueryResult executeQuery(ConnectorSession session, String sql, List<Object> params) {
return client.executeQuery(sql, params);
}

@Override
public ConnectorTableSchema getColumnsFromQuery(ConnectorSession session, String query) {
List<JdbcFieldInfo> fields = client.getColumnsFromQuery(query);
Expand Down
Loading
Loading