PluginDrivenMetadata.java
// 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.
package org.apache.doris.datasource.plugin;
import org.apache.doris.connector.api.Connector;
import org.apache.doris.connector.api.ConnectorMetadata;
import org.apache.doris.connector.api.ConnectorSession;
import org.apache.doris.connector.api.ConnectorStatementScope;
import java.util.Objects;
/**
* The single engine-side funnel through which every metadata acquisition in a statement flows, so that a
* statement uses exactly ONE {@link ConnectorMetadata} instance per catalog: it memoizes the result of
* {@code connector.getMetadata(session)} on the session's per-statement {@link ConnectorStatementScope} and hands
* every read / scan / DDL / MVCC resolver the same instance, closed deterministically at statement end.
*
* <p>Under {@link ConnectorStatementScope#NONE} (offline / no live statement) the scope memoizes nothing, so the
* factory runs on every call — {@code connector.getMetadata(session)} per call, byte-identical to today's behavior.
* The {@link ConnectorSession} is still built and passed to metadata operations as before; only the
* {@code getMetadata} result is memoized here, never the session.</p>
*
* <p>This class is the ONLY place in fe-core allowed to call {@code Connector#getMetadata} directly (an arch gate
* enforces that every other seam routes through here). A heterogeneous gateway connector that fans a foreign
* handle out to a sibling connector keys the memo by the owning connector as well as the catalog id; that
* sibling-aware entry is added when the gateway is wired.</p>
*/
public final class PluginDrivenMetadata {
private PluginDrivenMetadata() {
}
/**
* Returns the statement's single memoized {@link ConnectorMetadata} for {@code connector}, building it via
* {@code connector.getMetadata(session)} on first use and reusing it for the rest of the statement. Keyed by
* the session's catalog id, so a cross-catalog statement resolves each catalog independently.
*/
public static ConnectorMetadata get(ConnectorSession session, Connector connector) {
ConnectorStatementScope scope = session.getStatementScope();
long catalogId = session.getCatalogId();
// A statement resolves a catalog under exactly one identity (one statement = one user = one credential).
// Now that the write arm reuses the read arm's memoized instance, and a session=user connector bakes the
// querying user's delegated credential into that instance at getMetadata time, reusing it under a second
// identity would execute one user's operation with another's credentials. Pin the building identity and
// fail loud if a different one ever reuses the instance, turning a silent cross-user leak into a hard
// error. Uses the Doris principal (getUser), never a credential token, so fe-core parses no credentials.
// Under NONE this stores nothing, so the check is vacuously true and the factory runs on every call.
String user = session.getUser();
String builderUser = scope.computeIfAbsent("metadata-identity:" + catalogId, () -> user);
if (!Objects.equals(builderUser, user)) {
throw new IllegalStateException("Per-statement metadata identity mismatch for catalog " + catalogId
+ ": the instance was built for user '" + builderUser + "' but is being reused for user '"
+ user + "'. A statement must resolve a catalog under a single identity.");
}
return scope.getOrCreateMetadata("metadata:" + catalogId, () -> connector.getMetadata(session));
}
}