Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d275e7c
[SPARK-XXXXX][SQL] Support METRIC_VIEW on V2 view catalogs
chenwang-databricks Apr 28, 2026
b575053
[SPARK-XXXXX][SQL][FOLLOWUP] Address cloud-fan's metric-view review f…
chenwang-databricks Apr 29, 2026
0509d5b
[SPARK-XXXXX][SQL][FOLLOWUP] Address self-review + fix CI on PR #55487
chenwang-databricks Apr 29, 2026
fcdc19f
[SPARK-XXXXX][SQL][FOLLOWUP] Expand SELECT / DROP TABLE / SHOW CREATE…
chenwang-databricks Apr 29, 2026
aeee6a7
[SPARK-XXX][SQL] Fix CI failures: METRIC_VIEW table-type mapping, rec…
chenwang-databricks Apr 30, 2026
27f4368
[SPARK-XXX][SQL] Rename test catalog to TableViewCatalog / MetadataTa…
chenwang-databricks Apr 30, 2026
a1c2927
[SPARK-XXX][SQL] Fix MetricViewV2CatalogSuite + HMS metric-view round…
chenwang-databricks Apr 30, 2026
d4474e7
[SPARK-XXXXX][SQL] Public StructField.json / .fromJson + Column.fromS…
chenwang-databricks Apr 30, 2026
0b26f95
[SPARK-XXXXX][SQL][FOLLOWUP] Address cloud-fan round 2 review on PR #…
chenwang-databricks May 1, 2026
437b71d
[SPARK-XXXXX][SQL][FOLLOWUP] Address cloud-fan round 3 review on PR #…
chenwang-databricks May 4, 2026
66b13eb
[SPARK-XXXXX][SQL][FOLLOWUP] Fix CI failures from round 3 follow-up
chenwang-databricks May 4, 2026
e980f24
[SPARK-XXXXX][SQL][FOLLOWUP] Address cloud-fan round 4 review on PR #…
chenwang-databricks May 5, 2026
fd3d8b2
Update sql/core/src/main/scala/org/apache/spark/sql/execution/datasou…
chenwang-databricks May 5, 2026
90deee1
[SPARK-XXXXX][SQL][FOLLOWUP] Drop varargs in Dependency API; port DBR…
chenwang-databricks May 6, 2026
f2251d8
[SPARK-XXXXX][SQL][FOLLOWUP] Switch Dependency API to arrays per clou…
chenwang-databricks May 6, 2026
ed2db22
[SPARK-XXXXX][SQL][FOLLOWUP] Address cloud-fan round 6 review on PR #…
chenwang-databricks May 6, 2026
f22b27d
Apply suggestions from code review
cloud-fan May 6, 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
11 changes: 11 additions & 0 deletions common/utils/src/main/resources/error/error-conditions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4123,6 +4123,12 @@
},
"sqlState" : "KD002"
},
"INVALID_METRIC_VIEW_YAML" : {
"message" : [
"Failed to parse metric view YAML: <message>"
],
"sqlState" : "42K0L"
},
"INVALID_NAME_IN_USE_COMMAND" : {
"message" : [
"Invalid name '<name>' in <command> command. Reason: <reason>"
Expand Down Expand Up @@ -8261,6 +8267,11 @@
"The table <tableName> is a Spark data source table. Please use SHOW CREATE TABLE without AS SERDE instead."
]
},
"ON_METRIC_VIEW" : {
"message" : [
"The command is not supported on a metric view <tableName>."
]
},
"ON_TEMPORARY_VIEW" : {
"message" : [
"The command is not supported on a temporary view <tableName>."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 org.apache.spark.annotation.Evolving;

/**
* Represents a dependency of a SQL object such as a view or metric view.
* <p>
* A dependency is one of: {@link TableDependency} or {@link FunctionDependency}. The
* {@code sealed} declaration enforces this structurally.
* <p>
* Note: today the only producer in Spark itself is metric-view dependency extraction, which
* emits {@link TableDependency} only. {@link FunctionDependency} and the
* {@link #function(String[])} factory are exposed as groundwork for future producers
* (e.g. SQL UDF dependency tracking); consumers iterating a {@link DependencyList} received
* from Spark today should expect to see only {@link TableDependency} instances.
*
* @since 4.2.0
*/
@Evolving
public sealed interface Dependency permits TableDependency, FunctionDependency {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FunctionDependency is in the sealed permits list and exposed via the Dependency.function(...) factory below, but no producer in this PR ever emits one -- MetricViewHelper.collectTableDependencies only emits TableDependency. Two options: (a) drop FunctionDependency until it has a producer (the @Evolving annotation is meant to evolve before stabilizing, so adding it later is cheap); (b) keep it as groundwork but mention in the PR description so reviewers don't trip on the dead surface, and add a sentence to this class-level Javadoc noting that consumers may receive only TableDependency instances today.


/**
* Construct a {@link TableDependency} from the structural multi-part name of the dependent
* table. {@code nameParts} should contain at least one element; for catalog-managed tables
* the first element is typically the catalog name and subsequent elements are namespace
* components followed by the table name.
*/
static TableDependency table(String[] nameParts) {
return new TableDependency(nameParts);
}

/**
* Construct a {@link FunctionDependency} from the structural multi-part name of the
* dependent function. {@code nameParts} should contain at least one element; for
* catalog-managed functions the first element is typically the catalog name and subsequent
* elements are namespace components followed by the function name.
*/
static FunctionDependency function(String[] nameParts) {
return new FunctionDependency(nameParts);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.Arrays;
import java.util.Objects;

import org.apache.spark.annotation.Evolving;

/**
* A list of dependencies for a SQL object such as a view or metric view.
* <p>
* <ul>
* <li>When {@code null}, the dependency information is not provided.</li>
* <li>When the array is empty, dependencies are provided but the object has none.</li>
* <li>When the array is non-empty, each entry describes one dependency.</li>
* </ul>
* <p>
* Records' auto-generated {@code equals}/{@code hashCode} on array fields fall through to
* {@link Object#equals} (reference equality), so this record overrides them to use
* {@link Arrays#equals(Object[], Object[])} / {@link Arrays#hashCode(Object[])} on
* {@code dependencies}; per-element equality delegates to the element's overridden
* {@code equals} ({@link TableDependency} / {@link FunctionDependency} both implement value
* semantics on their {@code nameParts} array). The defensive-copy accessor override clones
* on read so callers cannot mutate the record's internal array.
*
* @param dependencies array of dependencies; must contain no null elements (defensive
* copy made; not validated element-wise -- callers passing nulls will
* surface NPEs in downstream consumers)
* @since 4.2.0
*/
@Evolving
public record DependencyList(Dependency[] dependencies) {

public DependencyList {
Objects.requireNonNull(dependencies, "dependencies must not be null");
dependencies = dependencies.clone();
}

/** Returns a defensive copy of the underlying dependencies array. */
@Override
public Dependency[] dependencies() { return dependencies.clone(); }

@Override
public boolean equals(Object o) {
return o instanceof DependencyList that && Arrays.equals(dependencies, that.dependencies);
}

@Override
public int hashCode() { return Arrays.hashCode(dependencies); }

@Override
public String toString() {
return "DependencyList[dependencies=" + Arrays.toString(dependencies) + "]";
}

public static DependencyList of(Dependency[] dependencies) {
return new DependencyList(dependencies);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.Arrays;
import java.util.Objects;

import org.apache.spark.annotation.Evolving;

/**
* A function dependency of a SQL object.
* <p>
* The dependent function is identified by its structural multi-part name. See
* {@link TableDependency} for the parts-form contract.
* <p>
* Records' auto-generated {@code equals}/{@code hashCode} on array fields fall through to
* {@link Object#equals} (reference equality), so this record overrides them to use
* {@link Arrays#equals(Object[], Object[])} / {@link Arrays#hashCode(Object[])} on
* {@code nameParts} and give value-based semantics. The defensive-copy accessor override
* also clones on read so callers cannot mutate the record's internal array.
*
* @param nameParts structural multi-part identifier; must be non-empty and contain no
* null elements (defensive copy made; not validated element-wise --
* callers passing nulls will surface NPEs in downstream consumers)
* @since 4.2.0
*/
@Evolving
public record FunctionDependency(String[] nameParts) implements Dependency {
public FunctionDependency {
Objects.requireNonNull(nameParts, "nameParts must not be null");
if (nameParts.length == 0) {
throw new IllegalArgumentException("nameParts must not be empty");
}
nameParts = nameParts.clone();
}

/** Returns a defensive copy of the underlying parts array. */
@Override
public String[] nameParts() { return nameParts.clone(); }

@Override
public boolean equals(Object o) {
return o instanceof FunctionDependency that && Arrays.equals(nameParts, that.nameParts);
}

@Override
public int hashCode() { return Arrays.hashCode(nameParts); }

@Override
public String toString() {
return "FunctionDependency[nameParts=" + Arrays.toString(nameParts) + "]";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.Arrays;
import java.util.Objects;

import org.apache.spark.annotation.Evolving;

/**
* A table dependency of a SQL object.
* <p>
* The dependent table is identified by its structural multi-part name. {@code nameParts}
* arity matches the catalog's namespace depth plus one for the table name -- for a catalog
* with single-level namespaces the parts are {@code [catalog, schema, table]}; for a catalog
* with multi-level namespaces (e.g. Iceberg with {@code db1.db2}) the parts are
* {@code [catalog, db1, db2, ..., table]}; for v1 sources resolved through the session
* catalog, producers should normalize to {@code [spark_catalog, db, table]} so consumers see
* a stable arity per source kind. The structural form preserves arity and is unambiguous
* against quoted identifiers containing a literal {@code .}; consumers that need a flat
* string should join the parts themselves with a quoting scheme appropriate to their wire
* format.
* <p>
* Records' auto-generated {@code equals}/{@code hashCode} on array fields fall through to
* {@link Object#equals} (reference equality), so this record overrides them to use
* {@link Arrays#equals(Object[], Object[])} / {@link Arrays#hashCode(Object[])} on
* {@code nameParts} and give value-based semantics. The defensive-copy accessor override
* also clones on read so callers cannot mutate the record's internal array.
*
* @param nameParts structural multi-part identifier; must be non-empty and contain no
* null elements (defensive copy made; not validated element-wise --
* callers passing nulls will surface NPEs in downstream consumers)
* @since 4.2.0
*/
@Evolving
public record TableDependency(String[] nameParts) implements Dependency {
public TableDependency {
Objects.requireNonNull(nameParts, "nameParts must not be null");
if (nameParts.length == 0) {
throw new IllegalArgumentException("nameParts must not be empty");
}
nameParts = nameParts.clone();
}

/** Returns a defensive copy of the underlying parts array. */
@Override
public String[] nameParts() { return nameParts.clone(); }

@Override
public boolean equals(Object o) {
return o instanceof TableDependency that && Arrays.equals(nameParts, that.nameParts);
}

@Override
public int hashCode() { return Arrays.hashCode(nameParts); }

@Override
public String toString() {
return "TableDependency[nameParts=" + Arrays.toString(nameParts) + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public interface TableSummary {
String EXTERNAL_TABLE_TYPE = "EXTERNAL";
String VIEW_TABLE_TYPE = "VIEW";
String FOREIGN_TABLE_TYPE = "FOREIGN";
String METRIC_VIEW_TABLE_TYPE = "METRIC_VIEW";

Identifier identifier();
String tableType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class ViewInfo extends TableInfo {
private final Map<String, String> sqlConfigs;
private final String schemaMode;
private final String[] queryColumnNames;
private final DependencyList viewDependencies;

protected ViewInfo(Builder builder) {
super(builder);
Expand All @@ -57,11 +58,11 @@ protected ViewInfo(Builder builder) {
this.sqlConfigs = Collections.unmodifiableMap(builder.sqlConfigs);
this.schemaMode = builder.schemaMode;
this.queryColumnNames = builder.queryColumnNames;
// Force PROP_TABLE_TYPE = VIEW so that `properties()` reflects the typed ViewInfo
// classification. Catalogs and generic viewers reading PROP_TABLE_TYPE from the properties
// bag (e.g. TableCatalog.listTableSummaries default impl, DESCRIBE) see "VIEW" without
// requiring authors to remember to call withTableType(VIEW).
properties().put(TableCatalog.PROP_TABLE_TYPE, TableSummary.VIEW_TABLE_TYPE);
this.viewDependencies = builder.viewDependencies;
// Default PROP_TABLE_TYPE = VIEW so `properties()` reflects the typed ViewInfo
// classification. Callers can refine to a more specific view kind (for example,
// METRIC_VIEW) by calling BaseBuilder.withTableType(...) on the builder before build().
properties().putIfAbsent(TableCatalog.PROP_TABLE_TYPE, TableSummary.VIEW_TABLE_TYPE);
}

/** The SQL text of the view. */
Expand Down Expand Up @@ -102,13 +103,22 @@ protected ViewInfo(Builder builder) {
*/
public String[] queryColumnNames() { return queryColumnNames; }

/**
* Returns the structured list of objects this view depends on (source tables and functions),
* or {@code null} if no dependency list was supplied. Unlike other view metadata which is
* encoded into {@link #properties()}, dependency lists are a first-class field because their
* nested structure does not round-trip cleanly through flat string properties.
*/
public DependencyList viewDependencies() { return viewDependencies; }

public static class Builder extends BaseBuilder<Builder> {
private String queryText;
private String currentCatalog;
private String[] currentNamespace = new String[0];
private Map<String, String> sqlConfigs = new HashMap<>();
private String schemaMode;
private String[] queryColumnNames = new String[0];
private DependencyList viewDependencies = null;

@Override
protected Builder self() { return this; }
Expand Down Expand Up @@ -143,6 +153,16 @@ public Builder withQueryColumnNames(String[] queryColumnNames) {
return this;
}

/**
* Sets the structured dependency list for this view. Source tables and functions referenced
* by the view text should be recorded here so downstream consumers (e.g. catalogs persisting
* lineage) can access them without re-analyzing the view body.
*/
public Builder withViewDependencies(DependencyList viewDependencies) {
this.viewDependencies = viewDependencies;
return this;
}

@Override
public ViewInfo build() {
Objects.requireNonNull(columns, "columns should not be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1226,7 +1226,7 @@ class Analyzer(
) {
CatalogV2Util.loadTable(catalog, ident).map {
case v1Table: V1Table if CatalogV2Util.isSessionCatalog(catalog) &&
v1Table.v1Table.tableType == CatalogTableType.VIEW =>
v1Table.v1Table.isViewLike =>
val v1Ident = v1Table.catalogTable.identifier
val v2Ident = Identifier.of(v1Ident.database.toArray, v1Ident.identifier)
ResolvedPersistentView(
Expand Down
Loading