Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7b2d54c
[SPARK-56655][SQL] Rename v2 metadata-table API: MetadataOnlyTable, R…
cloud-fan Apr 28, 2026
b27919c
[SPARK-56655][SQL] Implement remaining v2 view DDL and inspection com…
cloud-fan Apr 28, 2026
c121e07
[SPARK-56655][SQL][TESTS] Add per-catalog view command test triplets …
cloud-fan Apr 29, 2026
c649ef4
[SPARK-56655][SQL] Address self-review on v2 view DDL/inspection PR
cloud-fan Apr 29, 2026
b5cacaa
[SPARK-56655][SQL] Propagate namespace default collation through ALTE…
cloud-fan Apr 29, 2026
75375cf
[SPARK-56655][SQL] Self-review fixes: re-cache v2 view after RENAME, …
cloud-fan Apr 29, 2026
09e37e1
[SPARK-56655][SQL] Self-review fixes: collapse double loadNamespaceMe…
cloud-fan Apr 29, 2026
5173d01
[SPARK-56655][SQL][TESTS] Lift v1-parity collision tests for view DDL…
cloud-fan Apr 29, 2026
b6f974a
[SPARK-56655][SQL] Align v2 DROP-wrong-kind error class to v1 (WRONG_…
cloud-fan Apr 29, 2026
84260d6
[SPARK-56655][SQL] Fix CREATE TABLE LIKE on v2 view source via DataSo…
cloud-fan Apr 29, 2026
cf0abfa
[SPARK-56655][SQL] Self-review fixes: pin DESC TABLE PARTITION on v2 …
cloud-fan Apr 29, 2026
8784787
[SPARK-56655][SQL] Self-review fixes: SHOW COLUMNS namespace conflict…
cloud-fan Apr 29, 2026
dabfd4f
[SPARK-56655][SQL][TESTS] Lift two v2 view-DDL SQL-behavior tests int…
cloud-fan Apr 29, 2026
b16996c
[SPARK-56655][SQL] Make code comments self-contained
cloud-fan Apr 29, 2026
19b2d5b
[SPARK-56655][SQL] Stable plan-tree rendering for ResolvedPersistentV…
cloud-fan Apr 29, 2026
4dd59bb
Merge remote-tracking branch 'upstream/master' into v2-view-followup
cloud-fan Apr 29, 2026
83a38b4
[SPARK-56655][SQL][TESTS] Fix analyzer-test goldens for shared-catalo…
gengliangwang Apr 30, 2026
b7e8fba
[SPARK-56655][SQL] Fix Java linter line-length in MetadataTable.java
gengliangwang Apr 30, 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
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,31 @@
* (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 ViewInfo.Builder} (for views). A {@code MetadataTable} 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.
* data-source table; a {@code MetadataTable} wrapping a {@link ViewInfo} can be returned
* from {@link TableViewCatalog#loadTableOrView(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 {
public class MetadataTable 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
* returning a {@code MetadataTable} from {@link TableCatalog#loadTable} or
* {@link TableViewCatalog#loadTableOrView} should typically pass
* {@code ident.toString()}, matching the quoted multi-part form used elsewhere
* for v2 identifiers.
*/
public MetadataOnlyTable(TableInfo info, String name) {
public MetadataTable(TableInfo info, String name) {
this.info = Objects.requireNonNull(info, "info should not be null");
this.name = Objects.requireNonNull(name, "name should not be null");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* Catalog API for connectors that expose tables.
* <p>
* Connectors that expose <i>only</i> tables implement this interface. Connectors that expose
* both tables and views must implement {@link RelationCatalog} (which extends both this
* both tables and views must implement {@link TableViewCatalog} (which extends both this
* interface and {@link ViewCatalog} and adds the cross-cutting contract for the combined
* case); the methods on this interface remain table-only -- they do not interact with views.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
* 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};
* Connectors that expose <i>both</i> tables and views must implement {@code TableViewCatalog};
* implementing {@link TableCatalog} and {@link ViewCatalog} directly without
* {@code RelationCatalog} is rejected at catalog initialization. Connectors that expose only
* {@code TableViewCatalog} 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
* A {@code TableViewCatalog} 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
Expand Down Expand Up @@ -67,6 +67,9 @@
* <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>
* <tr><td>{@link ViewCatalog#renameView}</td>
* <td>a table sits at {@code newIdent}</td>
* <td>{@link org.apache.spark.sql.catalyst.analysis.ViewAlreadyExistsException}</td></tr>
* </table>
*
* <b>Passive filtering</b> (read / non-collision mutation methods that behave as if the wrong
Expand All @@ -91,17 +94,19 @@
* <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#renameView}</td>
* <td>throws {@code NoSuchViewException} when the source is a table</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
* questions in two round trips each. {@code TableViewCatalog} 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
* <li>{@link #loadTableOrView(Identifier)} -- the resolver's per-identifier read path. Returns
* a regular {@link Table} for a table, or a {@link MetadataTable} 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
Expand All @@ -113,22 +118,22 @@
* @since 4.2.0
*/
@Evolving
public interface RelationCatalog extends TableCatalog, ViewCatalog {
public interface TableViewCatalog 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
* {@link MetadataTable} 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
* @return a {@link Table} for tables, or a {@link MetadataTable} 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;
Table loadTableOrView(Identifier ident) throws NoSuchTableException;

/**
* List the tables and views in a namespace, returned as {@link TableSummary} entries with
Expand Down Expand Up @@ -162,14 +167,14 @@ default TableSummary[] listRelationSummaries(String[] namespace)
/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: a {@link MetadataOnlyTable}
* The default implementation derives from {@link #loadTableOrView}: a {@link MetadataTable}
* 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) {
Table t = loadTableOrView(ident);
if (t instanceof MetadataTable mot && mot.getTableInfo() instanceof ViewInfo) {
throw new NoSuchTableException(ident);
}
return t;
Expand All @@ -178,7 +183,7 @@ default Table loadTable(Identifier ident) throws NoSuchTableException {
/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: a {@link MetadataOnlyTable}
* The default implementation derives from {@link #loadTableOrView}: a {@link MetadataTable}
* 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.
Expand All @@ -187,11 +192,11 @@ default Table loadTable(Identifier ident) throws NoSuchTableException {
default ViewInfo loadView(Identifier ident) throws NoSuchViewException {
Table t;
try {
t = loadRelation(ident);
t = loadTableOrView(ident);
} catch (NoSuchTableException e) {
throw new NoSuchViewException(ident);
}
if (t instanceof MetadataOnlyTable mot && mot.getTableInfo() instanceof ViewInfo vi) {
if (t instanceof MetadataTable mot && mot.getTableInfo() instanceof ViewInfo vi) {
return vi;
}
throw new NoSuchViewException(ident);
Expand All @@ -200,14 +205,14 @@ default ViewInfo loadView(Identifier ident) throws NoSuchViewException {
/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: returns {@code true} only if
* The default implementation derives from {@link #loadTableOrView}: 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);
Table t = loadTableOrView(ident);
return !(t instanceof MetadataTable mot && mot.getTableInfo() instanceof ViewInfo);
} catch (NoSuchTableException e) {
return false;
}
Expand All @@ -216,14 +221,14 @@ default boolean tableExists(Identifier ident) {
/**
* {@inheritDoc}
* <p>
* The default implementation derives from {@link #loadRelation}: returns {@code true} only if
* The default implementation derives from {@link #loadTableOrView}: 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;
Table t = loadTableOrView(ident);
return t instanceof MetadataTable mot && mot.getTableInfo() instanceof ViewInfo;
} catch (NoSuchTableException e) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* Catalog API for connectors that expose views.
* <p>
* Connectors that expose <i>only</i> views implement this interface. Connectors that expose
* both tables and views must implement {@link RelationCatalog} (which extends both this
* both tables and views must implement {@link TableViewCatalog} (which extends both this
* interface and {@link TableCatalog} and adds the cross-cutting contract for the combined
* case); the methods on this interface remain view-only -- they do not interact with tables.
* <p>
Expand Down Expand Up @@ -126,7 +126,7 @@ ViewInfo createView(Identifier ident, ViewInfo info)
* concurrent {@code CREATE VIEW} won the race in the
* default impl's gap between {@link #replaceView} and
* the fallback {@link #createView}, or, in a
* {@link RelationCatalog}, a table sits at {@code ident}
* {@link TableViewCatalog}, a table sits at {@code ident}
* @throws NoSuchNamespaceException if the identifier's namespace does not exist (optional)
*/
default ViewInfo createOrReplaceView(Identifier ident, ViewInfo info)
Expand All @@ -145,4 +145,21 @@ default ViewInfo createOrReplaceView(Identifier ident, ViewInfo info)
* @return true if a view was dropped, false otherwise
*/
boolean dropView(Identifier ident);

/**
* Rename a view.
* <p>
* If the catalog supports tables and contains a table at the new identifier, this must throw
* {@link ViewAlreadyExistsException}. If the source identifier resolves to a table rather than
* a view, this must throw {@link NoSuchViewException}. The cross-type contract for catalogs
* that expose both tables and views lives on {@link TableViewCatalog}.
*
* @param oldIdent the view identifier of the existing view to rename
* @param newIdent the new view identifier
* @throws NoSuchViewException if no view exists at {@code oldIdent}
* @throws ViewAlreadyExistsException if a view (or, in a {@link TableViewCatalog}, a table)
* already exists at {@code newIdent}
*/
void renameView(Identifier oldIdent, Identifier newIdent)
throws NoSuchViewException, ViewAlreadyExistsException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
* query output column names. Schema and user TBLPROPERTIES are inherited from {@link TableInfo}
* via the typed builder.
* <p>
* {@code ViewInfo} extends {@link TableInfo} so that a {@link RelationCatalog} can opt into the
* single-RPC perf path by returning a {@link MetadataOnlyTable} wrapping a {@code ViewInfo}
* from {@link RelationCatalog#loadRelation} for a view identifier. Pure {@link ViewCatalog}
* {@code ViewInfo} extends {@link TableInfo} so that a {@link TableViewCatalog} can opt into the
* single-RPC perf path by returning a {@link MetadataTable} wrapping a {@code ViewInfo}
* from {@link TableViewCatalog#loadTableOrView} for a view identifier. Pure {@link ViewCatalog}
* implementations never see {@code TableInfo}; the typed setters on {@link Builder} cover
* everything they need to construct a {@code ViewInfo}.
*
Expand All @@ -49,7 +49,7 @@ public class ViewInfo extends TableInfo {
private final String schemaMode;
private final String[] queryColumnNames;

private ViewInfo(Builder builder) {
protected ViewInfo(Builder builder) {
super(builder);
this.queryText = Objects.requireNonNull(builder.queryText, "queryText should not be null");
this.currentCatalog = builder.currentCatalog;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1132,8 +1132,8 @@ class Analyzer(
* so surfacing a downstream "view not found" would hide the real reason.
*
* Lookup order against a non-session catalog:
* 1. If the catalog is a [[RelationCatalog]], [[RelationCatalog.loadRelation]] is called
* once. A returned [[MetadataOnlyTable]] wrapping a [[ViewInfo]] is interpreted as a
* 1. If the catalog is a [[TableViewCatalog]], [[TableViewCatalog.loadTableOrView]] is called
* once. A returned [[MetadataTable]] wrapping a [[ViewInfo]] is interpreted as a
* view; other results are tables.
* 2. Otherwise, [[TableCatalog.loadTable]] is tried (when implemented), then
* [[ViewCatalog.loadView]] as the fallback view-resolution path (when implemented).
Expand All @@ -1151,15 +1151,15 @@ class Analyzer(
throw QueryCompilationErrors.missingCatalogViewsAbilityError(catalog)
}
catalog match {
case mc: RelationCatalog =>
// Single-RPC perf path: loadRelation returns a Table for a table or a
// MetadataOnlyTable wrapping a ViewInfo for a view. NoSuchTable means
case mc: TableViewCatalog =>
// Single-RPC perf path: loadTableOrView returns a Table for a table or a
// MetadataTable wrapping a ViewInfo for a view. NoSuchTable means
// neither exists.
try {
Some(mc.loadRelation(ident) match {
case t: MetadataOnlyTable if t.getTableInfo.isInstanceOf[ViewInfo] =>
Some(mc.loadTableOrView(ident) match {
case t: MetadataTable if t.getTableInfo.isInstanceOf[ViewInfo] =>
ResolvedPersistentView(
catalog, ident, V1Table.toCatalogTable(catalog, ident, t))
catalog, ident, t.getTableInfo.asInstanceOf[ViewInfo])
case table =>
ResolvedTable.create(catalog.asTableCatalog, ident, table)
})
Expand All @@ -1180,7 +1180,7 @@ class Analyzer(
val v1Ident = v1Table.catalogTable.identifier
val v2Ident = Identifier.of(v1Ident.database.toArray, v1Ident.identifier)
ResolvedPersistentView(
catalog, v2Ident, v1Table.catalogTable)
catalog, v2Ident, new V1ViewInfo(v1Table.catalogTable))
case table =>
ResolvedTable.create(catalog.asTableCatalog, ident, table)
}
Expand All @@ -1191,9 +1191,7 @@ class Analyzer(
catalog match {
case vc: ViewCatalog =>
try {
val viewInfo = vc.loadView(ident)
val catalogTable = V1Table.toCatalogTable(catalog, ident, viewInfo)
Some(ResolvedPersistentView(catalog, ident, catalogTable))
Some(ResolvedPersistentView(catalog, ident, vc.loadView(ident)))
} catch {
case _: NoSuchViewException => None
}
Expand Down
Loading