Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
9f2570c
add GENERAL_TABLE v2 table capacity
cloud-fan Jul 9, 2025
914d81a
address comments
cloud-fan Jul 11, 2025
171a52a
address comment
cloud-fan Jul 29, 2025
647ec77
rename
cloud-fan Jul 30, 2025
3ab45a6
Apply suggestions from code review
cloud-fan Jul 31, 2025
31a8d44
address comment
cloud-fan Aug 1, 2025
807e616
add view.currentCatalog and view.currentNamespace
cloud-fan Apr 22, 2026
674d607
clarify PROP_VIEW_CURRENT_NAMESPACE encoding format
cloud-fan Apr 22, 2026
6193316
unify MetadataOnlyTable with TableInfo; add SUPPORTS_CREATE_VIEW capa…
cloud-fan Apr 22, 2026
6527622
make MetadataOnlyTable.properties() return an immutable view
cloud-fan Apr 22, 2026
0a86bcf
consolidate reserved-key list into CatalogV2Util.TABLE_RESERVED_PROPE…
cloud-fan Apr 22, 2026
4bf3bfc
collapse view currentCatalog/currentNamespace into a single property
cloud-fan Apr 22, 2026
a642356
implement DS v2 CREATE VIEW via TableCatalog.createTable
cloud-fan Apr 22, 2026
ed0896f
make CreateView an AnalysisOnlyCommand to capture referredTempFunctions
cloud-fan Apr 22, 2026
e9f834a
address self-review findings for v2 CREATE VIEW
cloud-fan Apr 22, 2026
710b97f
add ALTER VIEW support for DS v2 catalogs
cloud-fan Apr 22, 2026
eee8c49
rename SUPPORTS_CREATE_VIEW to SUPPORTS_VIEW
cloud-fan Apr 22, 2026
3ae6e65
address review findings: uncaching, viewSchemaMode, capability check,…
cloud-fan Apr 22, 2026
5d91480
fix multi-part namespace handling: fullIdent + cyclic-ref check in an…
cloud-fan Apr 22, 2026
db16d69
reject CREATE VIEW over a non-view table; preserve PROP_OWNER on ALTER
cloud-fan Apr 22, 2026
206f2dd
address self-review findings: simplify Analyzer and strategy, tighten…
cloud-fan Apr 23, 2026
da417f5
address self-review findings: v2 SHOW VIEWS, orphan-plan pinning, API…
cloud-fan Apr 23, 2026
5d38bd0
address self-review findings: extract SUPPORTS_VIEW helper, fix test …
cloud-fan Apr 23, 2026
6329d77
address self-review findings: pin more orphan v2-view plans, route DR…
cloud-fan Apr 23, 2026
6f1e4a7
update 'View commands are not supported' test for the new error shape
cloud-fan Apr 23, 2026
ce557d7
restore viewOnly gate with SUPPORTS_VIEW carve-out; unify view-DDL re…
cloud-fan Apr 23, 2026
1a532b9
address self-review findings: require explicit MetadataOnlyTable name…
cloud-fan Apr 23, 2026
c2fccb0
address self-review findings: stamp PROP_OWNER on v2 CREATE VIEW, pre…
cloud-fan Apr 23, 2026
2182176
address self-review findings: rework v2 view API to ViewInfo; fix mul…
cloud-fan Apr 23, 2026
4cca4e0
address self-review findings: DropViewExec type check; multi-part nam…
cloud-fan Apr 23, 2026
2e9b6bb
fix tests for new error class and CreateView field
cloud-fan Apr 23, 2026
283d05f
Merge remote-tracking branch 'upstream/master' into v1-v2
cloud-fan Apr 24, 2026
b4a40bf
fix DescribeRelation pattern arity after SPARK-39660 merge
cloud-fan Apr 24, 2026
f76d92a
unblock javadoc generation: downgrade scaladoc on public-package methods
cloud-fan Apr 24, 2026
2c4edd4
rework: separate ViewCatalog interface, drop SUPPORTS_VIEW
cloud-fan Apr 25, 2026
966f0c7
tests: adapt MetadataOnlyView suite catalogs to mixed TableCatalog+Vi…
cloud-fan Apr 25, 2026
66fa409
fix: pure ViewCatalog support in resolver; drop misleading staging-ex…
cloud-fan Apr 25, 2026
6bbb3c9
address self-review findings: v1-parity for CREATE VIEW IF NOT EXISTS…
cloud-fan Apr 25, 2026
a088c5c
address self-review findings: minor Javadoc wording
cloud-fan Apr 25, 2026
67e5890
fix RelationResolution import: TableCatalog missing after pure-ViewCa…
cloud-fan Apr 25, 2026
93241b5
remove unused CatalogV2Util imports flagged by -Wconf unused-imports
cloud-fan Apr 25, 2026
d830eba
fix: scalastyle import order; tests must use createView/viewExists
cloud-fan Apr 25, 2026
f0f6e46
DropTableExec: dropTable-first + viewExists fallback for EXPECT_TABLE…
cloud-fan Apr 26, 2026
f894e3c
ViewCatalog.createOrReplaceView for CREATE OR REPLACE VIEW (single-RP…
cloud-fan Apr 26, 2026
62b2613
address self-review findings: minor Javadoc grammar fix
cloud-fan Apr 26, 2026
8343224
address self-review findings: drop "schema mode" from transient-field…
cloud-fan Apr 26, 2026
08067be
address self-review findings: minor Scaladoc grammar fix in DropTable…
cloud-fan Apr 26, 2026
903a495
add RelationCatalog: dedicated interface for catalogs serving both ta…
cloud-fan Apr 26, 2026
38ffa07
RelationCatalog: derive loadTable/loadView/tableExists/viewExists fro…
cloud-fan Apr 26, 2026
417e8fa
test fixtures: rename TestingViewCatalog -> TestingRelationCatalog; d…
cloud-fan Apr 26, 2026
9579c36
DropTableExec: guard purgeTable behind tableExists for IF EXISTS no-o…
cloud-fan Apr 26, 2026
d3bd038
DropTableExec: restore upfront tableExists for both purge and dropTab…
cloud-fan Apr 27, 2026
45783b9
Fix scalastyle: wrap long Scaladoc/string lines in Analyzer and Catalogs
cloud-fan Apr 27, 2026
f680aa2
RelationCatalog: fix @inheritDoc Javadoc tag (block -> inline form)
cloud-fan Apr 27, 2026
7d833a0
RelationCatalog: replace {@inheritDoc} on default-method overrides wi…
cloud-fan Apr 27, 2026
8adcf25
DEBUG: force JVM exception logging in javadoc to surface silent tree-…
cloud-fan Apr 27, 2026
1af03b0
DEBUG: also log class loads to identify the symbol failing javac comp…
cloud-fan Apr 28, 2026
7ce564b
DEBUG: switch to javadoc -verbose to see user class file reads
cloud-fan Apr 28, 2026
df73eba
RelationCatalog: fix javadoc heading sequence (h3 -> h2); drop debug …
cloud-fan Apr 28, 2026
57af4e1
RelationCatalog: restore {@inheritDoc} on default-method overrides
cloud-fan Apr 28, 2026
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
Comment thread
gengliangwang marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just MetadataTable?

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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.spark.sql.connector.catalog;

import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.spark.annotation.Evolving;
import org.apache.spark.sql.connector.catalog.constraints.Constraint;
import org.apache.spark.sql.connector.expressions.Transform;

/**
* A concrete {@code Table} implementation that contains only table metadata, deferring
* read/write to Spark. It represents a general Spark data source table or a Spark view;
* Spark resolves the table provider into a data source (for tables) or expands the view text
* (for views) at read time.
* <p>
* Catalogs build the metadata via {@link TableInfo.Builder} (for data-source tables) or
* {@link ViewInfo.Builder} (for views). A {@code MetadataOnlyTable} wrapping a
* {@link TableInfo} can be returned from {@link TableCatalog#loadTable(Identifier)} for a
* data-source table; a {@code MetadataOnlyTable} wrapping a {@link ViewInfo} can be returned
* from {@link RelationCatalog#loadRelation(Identifier)} as the single-RPC perf opt-in for a view.
* Downstream consumers distinguish the two by checking
* {@code getTableInfo() instanceof ViewInfo}.
*
* @since 4.2.0
*/
@Evolving
public class MetadataOnlyTable implements Table {
private final TableInfo info;
private final String name;

/**
* @param info metadata for the table or view. Pass a {@link ViewInfo} for a view.
* @param name human-readable name for this table, used by places that read {@link #name()}
* (e.g. the {@code Name} row of {@code DESCRIBE TABLE EXTENDED}). Catalogs
* returning a {@code MetadataOnlyTable} from {@link TableCatalog#loadTable} or
* {@link RelationCatalog#loadRelation} should typically pass
* {@code ident.toString()}, matching the quoted multi-part form used elsewhere
* for v2 identifiers.
*/
public MetadataOnlyTable(TableInfo info, String name) {
this.info = Objects.requireNonNull(info, "info should not be null");
this.name = Objects.requireNonNull(name, "name should not be null");
}

public TableInfo getTableInfo() {
return info;
}

@Override
public Column[] columns() {
return info.columns();
}

@Override
public Map<String, String> properties() {
return Collections.unmodifiableMap(info.properties());
}

@Override
public Transform[] partitioning() {
return info.partitions();
}

@Override
public Constraint[] constraints() {
return info.constraints();
}

@Override
public String name() {
return name;
}

@Override
public Set<TableCapability> capabilities() {
return Set.of();
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sort of general. How about TableViewCatalog?

Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* 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.spark.sql.connector.catalog;

import java.util.ArrayList;

import org.apache.spark.annotation.Evolving;
import org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException;
import org.apache.spark.sql.catalyst.analysis.NoSuchTableException;
import org.apache.spark.sql.catalyst.analysis.NoSuchViewException;

/**
* Catalog API for connectors that expose both tables and views in a single shared identifier
* namespace.
* <p>
* Connectors that expose <i>both</i> tables and views must implement {@code RelationCatalog};
* implementing {@link TableCatalog} and {@link ViewCatalog} directly without
* {@code RelationCatalog} is rejected at catalog initialization. Connectors that expose only
* tables implement just {@link TableCatalog}; connectors that expose only views implement just
* {@link ViewCatalog}; this interface is not relevant to them.
*
* <h2>Two principles</h2>
*
* A {@code RelationCatalog} follows two rules that, taken together, define every cross-cutting
* subtlety:
* <ol>
* <li><b>Orthogonal interfaces.</b> Every {@link TableCatalog} method behaves as if views did
* not exist, and every {@link ViewCatalog} method behaves as if tables did not exist.
* From the perspective of a {@code TableCatalog} caller, a view at an identifier is
* indistinguishable from "nothing there"; symmetrically for {@code ViewCatalog} on
* tables. The implementation, of course, knows about both kinds -- it just filters them
* apart at each method boundary.</li>
* <li><b>Single identifier namespace.</b> Tables and views share one keyspace within a
* namespace; the same {@link Identifier} cannot resolve to both at the same time. The
* implementation typically enforces this with a single backing keyspace plus a kind
* discriminator.</li>
* </ol>
*
* <h2>Per-method cross-type behavior</h2>
*
* <b>Active rejection</b> (write-side methods that throw on cross-type collision):
* <table>
* <caption>Cross-type rejection</caption>
* <tr><th>Method</th><th>Rejects when</th><th>Throws</th></tr>
* <tr><td>{@link TableCatalog#createTable}</td><td>a view sits at {@code ident}</td>
* <td>{@link org.apache.spark.sql.catalyst.analysis.TableAlreadyExistsException}</td></tr>
* <tr><td>{@link TableCatalog#renameTable}</td>
* <td>a view sits at {@code newIdent}</td>
* <td>{@link org.apache.spark.sql.catalyst.analysis.TableAlreadyExistsException}</td></tr>
* <tr><td>{@link ViewCatalog#createView}</td><td>a table sits at {@code ident}</td>
* <td>{@link org.apache.spark.sql.catalyst.analysis.ViewAlreadyExistsException}</td></tr>
* <tr><td>{@link ViewCatalog#createOrReplaceView}</td><td>a table sits at {@code ident}</td>
* <td>{@link org.apache.spark.sql.catalyst.analysis.ViewAlreadyExistsException}</td></tr>
* <tr><td>{@link ViewCatalog#replaceView}</td><td>a table sits at {@code ident}</td>
* <td>{@link org.apache.spark.sql.catalyst.analysis.NoSuchViewException}</td></tr>
* </table>
*
* <b>Passive filtering</b> (read / non-collision mutation methods that behave as if the wrong
* kind doesn't exist):
* <table>
* <caption>Cross-type filtering</caption>
* <tr><th>Method</th><th>On wrong-kind ident</th></tr>
* <tr><td>{@link TableCatalog#loadTable(Identifier)}</td>
* <td>throws {@code NoSuchTableException} for a view</td></tr>
* <tr><td>{@link TableCatalog#loadTable(Identifier, String)} /
* {@link TableCatalog#loadTable(Identifier, long)}</td>
* <td>throws {@code NoSuchTableException} for a view (no perf opt-in -- time-travel does
* not apply to views)</td></tr>
* <tr><td>{@link TableCatalog#tableExists}</td><td>returns {@code false} for a view</td></tr>
* <tr><td>{@link TableCatalog#dropTable} / {@link TableCatalog#purgeTable}</td>
* <td>returns {@code false} for a view; does not drop it</td></tr>
* <tr><td>{@link TableCatalog#renameTable}</td>
* <td>throws {@code NoSuchTableException} when the source is a view</td></tr>
* <tr><td>{@link TableCatalog#listTables}</td><td>tables only</td></tr>
* <tr><td>{@link ViewCatalog#loadView}</td>
* <td>throws {@code NoSuchViewException} for a table</td></tr>
* <tr><td>{@link ViewCatalog#viewExists}</td><td>returns {@code false} for a table</td></tr>
* <tr><td>{@link ViewCatalog#dropView}</td>
* <td>returns {@code false} for a table; does not drop it</td></tr>
* <tr><td>{@link ViewCatalog#listViews}</td><td>views only</td></tr>
* </table>
*
* <h2>Single-RPC perf entry points</h2>
*
* The orthogonal {@link TableCatalog} and {@link ViewCatalog} answer two cross-cutting
* questions in two round trips each. {@code RelationCatalog} adds dedicated methods so a
* catalog can answer both in one round trip:
* <ul>
* <li>{@link #loadRelation(Identifier)} -- the resolver's per-identifier read path. Returns
* a regular {@link Table} for a table, or a {@link MetadataOnlyTable} wrapping a
* {@link ViewInfo} for a view. Saves the {@code loadTable} -> {@code loadView} fallback
* on a cold cache.</li>
* <li>{@link #listRelationSummaries(String[])} -- a unified listing of tables and views with the
* kind preserved on each {@link TableSummary}. Default impl performs both
* {@link TableCatalog#listTableSummaries} and {@link ViewCatalog#listViews}; override to
* fetch in one round trip.</li>
* </ul>
*
* @since 4.2.0
*/
@Evolving
public interface RelationCatalog extends TableCatalog, ViewCatalog {

/**
* Load metadata for an identifier that may resolve to either a table or a view.
* <p>
* For a table, returns the table's {@link Table}. For a view, returns a
* {@link MetadataOnlyTable} wrapping a {@link ViewInfo}; callers discriminate via
* {@code getTableInfo() instanceof ViewInfo}. This lets the resolver answer in a single RPC
* instead of falling back from {@link TableCatalog#loadTable} to {@link ViewCatalog#loadView}.
*
* @param ident the identifier
* @return a {@link Table} for tables, or a {@link MetadataOnlyTable} wrapping a
* {@link ViewInfo} for views
* @throws NoSuchTableException if neither a table nor a view exists at {@code ident}
*/
Table loadRelation(Identifier ident) throws NoSuchTableException;

/**
* List the tables and views in a namespace, returned as {@link TableSummary} entries with
* the kind preserved on each summary.
* <p>
* The default implementation enumerates via {@link TableCatalog#listTableSummaries} for
* tables and {@link ViewCatalog#listViews} for views (two round trips). Catalogs that can
* fetch the unified listing in a single round trip should override.
*
* @param namespace a multi-part namespace
* @return an array of summaries for both tables and views in the namespace
* @throws NoSuchNamespaceException if the namespace does not exist (optional)
* @throws NoSuchTableException if a table listed by the underlying enumeration disappears
* before its summary can be assembled (default impl only)
*/
default TableSummary[] listRelationSummaries(String[] namespace)
throws NoSuchNamespaceException, NoSuchTableException {
TableSummary[] tableSummaries = listTableSummaries(namespace);
Identifier[] viewIdentifiers = listViews(namespace);
ArrayList<TableSummary> all = new ArrayList<>(
tableSummaries.length + viewIdentifiers.length);
for (TableSummary s : tableSummaries) {
all.add(s);
}
for (Identifier id : viewIdentifiers) {
all.add(TableSummary.of(id, TableSummary.VIEW_TABLE_TYPE));
}
return all.toArray(TableSummary[]::new);
}

/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: a {@link MetadataOnlyTable}
* wrapping a {@link ViewInfo} is rejected as not-a-table; anything else is returned. Override
* only if a tables-only path is materially cheaper than the unified one.
*/
@Override
default Table loadTable(Identifier ident) throws NoSuchTableException {
Table t = loadRelation(ident);
if (t instanceof MetadataOnlyTable mot && mot.getTableInfo() instanceof ViewInfo) {
throw new NoSuchTableException(ident);
}
return t;
}

/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: a {@link MetadataOnlyTable}
* wrapping a {@link ViewInfo} is unwrapped and returned; anything else (table or absent) is
* surfaced as {@link NoSuchViewException}. Override only if a views-only path is materially
* cheaper than the unified one.
*/
@Override
default ViewInfo loadView(Identifier ident) throws NoSuchViewException {
Table t;
try {
t = loadRelation(ident);
} catch (NoSuchTableException e) {
throw new NoSuchViewException(ident);
}
if (t instanceof MetadataOnlyTable mot && mot.getTableInfo() instanceof ViewInfo vi) {
return vi;
}
throw new NoSuchViewException(ident);
}

/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: returns {@code true} only if
* the entry exists and is not a view. Override only if a cheaper existence-check path exists.
*/
@Override
default boolean tableExists(Identifier ident) {
try {
Table t = loadRelation(ident);
return !(t instanceof MetadataOnlyTable mot && mot.getTableInfo() instanceof ViewInfo);
} catch (NoSuchTableException e) {
return false;
}
}

/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: returns {@code true} only if
* the entry exists and is a view. Override only if a cheaper existence-check path exists.
*/
@Override
default boolean viewExists(Identifier ident) {
try {
Table t = loadRelation(ident);
return t instanceof MetadataOnlyTable mot && mot.getTableInfo() instanceof ViewInfo;
} catch (NoSuchTableException e) {
return false;
}
}
}
Loading