cloud-fan commented on code in PR #55487:
URL: https://github.com/apache/spark/pull/55487#discussion_r3156204831
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/command/metricViewCommands.scala:
##########
@@ -65,6 +83,66 @@ case class CreateMetricViewCommand(
ignoreIfExists = allowExisting)
Seq.empty
}
+
+ private def createMetricViewInV2Catalog(
+ sparkSession: SparkSession,
+ resolved: ResolvedIdentifier): Seq[Row] = {
+ // Metric views are persisted through the same `ViewCatalog` interface as
plain views; the
+ // only differences are `PROP_TABLE_TYPE = METRIC_VIEW` (so
`V1Table.toCatalogTable` maps the
+ // round-tripped row back to `CatalogTableType.METRIC_VIEW`), the
`metric_view.*` descriptor
+ // properties produced by `MetricView.getProperties`, and the typed
`viewDependencies` field.
+ val viewCatalog = resolved.catalog match {
+ case vc: ViewCatalog => vc
+ case other =>
+ throw QueryCompilationErrors.missingCatalogViewsAbilityError(other)
+ }
+ val ident = resolved.identifier
+ val name = ident.asTableIdentifier
+
+ val analyzed = MetricViewHelper.analyzeMetricViewText(sparkSession, name,
originalText)
+ validateUserColumns(name, analyzed)
+
+ // `retainMetadata = true` preserves the per-column `metric_view.type` /
`metric_view.expr`
+ // metadata that `ResolveMetricView.buildMetricViewOutput` attaches, even
when the user
+ // supplies column names with comments (same contract as the V1
session-catalog path, which
+ // goes through `ViewHelper.prepareTable` with `isMetricView = true`).
+ val aliasedSchema = ViewHelper
+ .aliasPlan(sparkSession, analyzed, userSpecifiedColumns, retainMetadata
= true)
+ .schema
Review Comment:
Compared with `V2ViewPreparation.buildViewInfo` for plain v2 views, the v2
metric-view path is missing:
- `CharVarcharUtils.getRawSchema(...)` on the aliased schema before passing
it to the builder.
- `SchemaUtils.checkColumnNameDuplication(...)`.
- `SchemaUtils.checkIndeterminateCollationInSchema(query.schema)`.
- `withOwner(CurrentUserContext.getCurrentUser)` — v1 (`prepareTable` →
`CatalogTable`) and plain v2 (`CreateV2ViewExec`) both stamp the owner, but
here the captured `ViewInfo` has no `PROP_OWNER`.
All four are exactly what `V2ViewPreparation.buildViewInfo` already does —
the cleanest fix is to share that code rather than re-add these one by one.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/command/metricViewCommands.scala:
##########
@@ -65,6 +83,66 @@ case class CreateMetricViewCommand(
ignoreIfExists = allowExisting)
Seq.empty
}
+
+ private def createMetricViewInV2Catalog(
+ sparkSession: SparkSession,
+ resolved: ResolvedIdentifier): Seq[Row] = {
+ // Metric views are persisted through the same `ViewCatalog` interface as
plain views; the
+ // only differences are `PROP_TABLE_TYPE = METRIC_VIEW` (so
`V1Table.toCatalogTable` maps the
+ // round-tripped row back to `CatalogTableType.METRIC_VIEW`), the
`metric_view.*` descriptor
+ // properties produced by `MetricView.getProperties`, and the typed
`viewDependencies` field.
+ val viewCatalog = resolved.catalog match {
+ case vc: ViewCatalog => vc
+ case other =>
+ throw QueryCompilationErrors.missingCatalogViewsAbilityError(other)
+ }
+ val ident = resolved.identifier
+ val name = ident.asTableIdentifier
+
+ val analyzed = MetricViewHelper.analyzeMetricViewText(sparkSession, name,
originalText)
+ validateUserColumns(name, analyzed)
+
+ // `retainMetadata = true` preserves the per-column `metric_view.type` /
`metric_view.expr`
+ // metadata that `ResolveMetricView.buildMetricViewOutput` attaches, even
when the user
+ // supplies column names with comments (same contract as the V1
session-catalog path, which
+ // goes through `ViewHelper.prepareTable` with `isMetricView = true`).
+ val aliasedSchema = ViewHelper
+ .aliasPlan(sparkSession, analyzed, userSpecifiedColumns, retainMetadata
= true)
+ .schema
+
+ // Describe this metric view's source and filter as user-visible
properties so catalogs and
+ // tooling can inspect them without re-parsing the YAML body.
+ val metricView = MetricViewFactory.fromYAML(originalText)
+ val viewProperties = new java.util.HashMap[String, String]()
+ properties.foreach { case (k, v) => viewProperties.put(k, v) }
+ metricView.getProperties.foreach { case (k, v) => viewProperties.put(k, v)
}
+ viewProperties.put(TableCatalog.PROP_TABLE_TYPE,
TableSummary.METRIC_VIEW_TABLE_TYPE)
+
+ val sourceTableNames = MetricViewHelper.collectTableDependencies(analyzed)
+ val deps = if (sourceTableNames.nonEmpty) {
+ DependencyList.of(sourceTableNames.map(Dependency.table): _*)
+ } else {
+ null
+ }
+
+ val manager = sparkSession.sessionState.catalogManager
+ val builder = new ViewInfo.Builder()
+ .withSchema(aliasedSchema)
+ .withProperties(viewProperties)
+ .withQueryText(originalText)
+ .withCurrentCatalog(manager.currentCatalog.name)
+ .withCurrentNamespace(manager.currentNamespace)
+ .withSqlConfigs(
+ ViewHelper.sqlConfigsToProps(sparkSession.sessionState.conf,
"").asJava)
+ .withSchemaMode(SchemaUnsupported.toString)
+ .withQueryColumnNames(analyzed.output.map(_.name).toArray)
+ .withViewDependencies(deps)
+ comment.foreach(builder.withComment)
+
+ viewCatalog.createView(ident, builder.build())
Review Comment:
`allowExisting` and `replace` are silently ignored here:
- `CREATE VIEW IF NOT EXISTS … WITH METRICS` on a v2 catalog will throw
`ViewAlreadyExistsException` instead of being a no-op — regression vs. v1
(`createMetricViewInSessionCatalog` passes `ignoreIfExists = allowExisting`).
- `CREATE OR REPLACE VIEW … WITH METRICS` will likewise throw instead of
replacing.
- If a *table* sits at the ident (mixed `RelationCatalog`), the
`ViewAlreadyExistsException` from `RelationCatalog.createView` will leak as a
generic "already exists" instead of the dedicated
`unsupportedCreateOrReplaceViewOnTableError` that `CreateV2ViewExec`'s catch
block (CreateV2ViewExec.scala:153-169) decodes via `tableExists`.
`CreateV2ViewExec.run` shows the full pattern: `viewExists` short-circuit on
`IF NOT EXISTS`, then `createOrReplaceView` vs `createView`, with the catch
block handling the table-collision case. None of the three paths is covered by
the new test suite today — see comment there.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/catalog/interface.scala:
##########
@@ -1089,8 +1085,9 @@ object CatalogTableType {
val EXTERNAL = new CatalogTableType("EXTERNAL")
val MANAGED = new CatalogTableType("MANAGED")
val VIEW = new CatalogTableType("VIEW")
+ val METRIC_VIEW = new CatalogTableType("METRIC_VIEW")
Review Comment:
Adding `METRIC_VIEW` as a separate `CatalogTableType` means every existing
`tableType == CatalogTableType.VIEW` site needs to be audited for whether it
should also accept `METRIC_VIEW`. Two visible regressions I noticed in
unchanged code:
**(a) `CatalogTable.toJsonLinkedHashMap` (line 670 of this file).** The
block emitting `"View Text"`, `"View Original Text"`, `"View Schema Mode"`,
`"View Catalog and Namespace"`, and `"SQL Path"` is gated on `tableType ==
CatalogTableType.VIEW`. With this PR's change to `tableType = METRIC_VIEW`, all
of those rows disappear from `DESCRIBE TABLE EXTENDED` output — a user-visible
regression vs. the pre-PR `VIEW + viewWithMetrics=true` storage. Likely fix:
change to `if (tableType == VIEW || tableType == METRIC_VIEW)`.
**(b) `HiveExternalCatalog`.** Three sites only handle `VIEW`:
- `createTable` (lines 277, 297) — empty-schema fallback for
Hive-incompatible types.
- `alterTable` (line 598) — VIEW-specific alter path.
- `restoreTableMetadata` (line 838) — read-back falls into
`restoreHiveSerdeTable` instead of restoring schema from view properties.
Tests use the in-memory catalog so the Hive path isn't exercised, but a
Hive-metastore-backed session-catalog metric view (the realistic deployment)
won't round-trip correctly.
Worth a repo-wide grep for `tableType == CatalogTableType.VIEW` / `tableType
== VIEW` to enumerate the rest before merging.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/command/metricViewCommands.scala:
##########
@@ -65,6 +83,66 @@ case class CreateMetricViewCommand(
ignoreIfExists = allowExisting)
Seq.empty
}
+
+ private def createMetricViewInV2Catalog(
Review Comment:
This new method open-codes a v2 view-CREATE path inside a v1
`RunnableCommand`. The plain-view v2 path is already factored:
`V2ViewPreparation.buildViewInfo()` (CreateV2ViewExec.scala:62-110) does almost
exactly what this method's lines 109-138 do — alias plan, schema, properties,
currentCatalog/Namespace, sqlConfigs, schemaMode, queryColumnNames, comment —
and `CreateV2ViewExec` adds the IF NOT EXISTS / OR REPLACE /
cross-type-collision handling.
Would you consider refactoring this into either (a) a
`CreateV2MetricViewExec extends V2ViewPreparation` exec invoked by a
`DataSourceV2Strategy` rule that matches `CreateMetricViewCommand` on a
non-session catalog, or (b) extending `V2ViewPreparation` with a
`viewDependencies` / `extraProperties` hook that `CreateV2ViewExec` ignores and
a metric-view subclass populates? Either way the metric-view path picks up the
same flag handling and validation as plain v2 views without copy-paste.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/metricview/util/MetricViewPlanner.scala:
##########
@@ -65,9 +64,11 @@ object MetricViewPlanner {
MetricViewFactory.fromYAML(yaml)
} catch {
case e: MetricViewValidationException =>
- throw QueryCompilationErrors.invalidLiteralForWindowDurationError()
+ throw SparkException.internalError(
+ s"Invalid metric view YAML: ${e.getMessage}", e)
case e: MetricViewYAMLParsingException =>
- throw QueryCompilationErrors.invalidLiteralForWindowDurationError()
+ throw SparkException.internalError(
Review Comment:
Replacing `invalidLiteralForWindowDurationError` is good — that one was
definitely wrong — but `SparkException.internalError(...)` is also the wrong
category. A malformed user-supplied YAML body in `CREATE VIEW … LANGUAGE YAML
AS $$ … $$` is user input error, not a Spark internal bug; `internalError`
renders with framing along the lines of "please contact support / file a bug."
Recommend a `QueryCompilationErrors.invalidMetricViewYAMLError(...)` (or
similar) routed through `AnalysisException` with a real error class, so the
message is identified as a user-correctable problem. Same applies to the
`MetricViewValidationException` branch immediately above.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/MetricViewV2CatalogSuite.scala:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.execution
+
+import java.util.concurrent.ConcurrentHashMap
+
+import scala.jdk.CollectionConverters._
+
+import org.apache.spark.sql.{AnalysisException, QueryTest}
+import org.apache.spark.sql.catalyst.analysis.{NoSuchViewException,
ViewAlreadyExistsException}
+import org.apache.spark.sql.connector.catalog.{Identifier,
InMemoryTableCatalog, MetadataOnlyTable, RelationCatalog, Table, TableCatalog,
TableDependency, TableSummary, ViewInfo}
+import org.apache.spark.sql.metricview.serde.{AssetSource, Column, Constants,
DimensionExpression, MeasureExpression, MetricView, MetricViewFactory,
SQLSource}
+import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.sql.types.Metadata
+
+/**
+ * Tests that exercise
[[org.apache.spark.sql.execution.command.CreateMetricViewCommand]] on a
+ * non-session V2 catalog. Metric views are persisted through the same
[[ViewCatalog]] interface
+ * as plain views; the only marker that distinguishes them is `PROP_TABLE_TYPE
= METRIC_VIEW`
+ * plus the typed `viewDependencies` field on [[ViewInfo]]. The recording
catalog used here is a
+ * minimal [[RelationCatalog]] so the same instance can also host the source
table referenced by
+ * the metric view's YAML.
+ */
+class MetricViewV2CatalogSuite extends QueryTest with SharedSparkSession {
Review Comment:
Test gaps worth filling — especially the first three, which would have
caught the silent-flag-ignore findings on the create path:
- `CREATE OR REPLACE VIEW … WITH METRICS` on the v2 catalog (currently
throws `ViewAlreadyExistsException` instead of replacing).
- `CREATE VIEW IF NOT EXISTS … WITH METRICS` on the v2 catalog (regression
vs. v1; currently throws instead of no-op).
- `CREATE VIEW … WITH METRICS` against a v2 catalog where a *table* sits at
the target ident — should produce the dedicated cross-type-collision error, not
a generic `ViewAlreadyExistsException`.
- A V1 source dependency: a v2 metric view referencing a session-catalog
table (`HiveTableRelation` / `LogicalRelation`). Surfaces what
`tableFullName()` actually contains for that case.
- A multi-part-namespace V2 source: a metric view whose source identifier is
`cat.ns1.ns2.tbl`. Surfaces what `mkString(".")` produces for `tableFullName()`.
- Read-back: `SELECT * FROM <v2 metric view>` (or at least `EXPLAIN` /
`DESCRIBE EXTENDED`) so the `loadRelation` → `MetadataOnlyTable` →
`V1Table.toCatalogTable(ViewInfo)` round-trip is exercised end-to-end. The
capture-only fixture pins what flows INTO `createView` but not what comes back
OUT.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/command/metricViewCommands.scala:
##########
@@ -73,6 +151,37 @@ case class CreateMetricViewCommand(
case class AlterMetricViewCommand(child: LogicalPlan, originalText: String)
object MetricViewHelper {
+
+ /**
+ * Walks the analyzed plan to collect direct table/view dependencies.
+ * Stops recursion at relation leaf nodes and persistent View nodes so that
only
+ * direct (not transitive) dependencies are recorded.
+ */
+ private[execution] def collectTableDependencies(plan: LogicalPlan):
Seq[String] = {
+ val tables = scala.collection.mutable.ArrayBuffer.empty[String]
+ def traverse(p: LogicalPlan): Unit = p match {
+ case v: View if !v.isTempView =>
+ tables += v.desc.identifier.unquotedString
+ case r: DataSourceV2Relation if r.catalog.isDefined &&
r.identifier.isDefined =>
+ val cat = r.catalog.get.name()
+ val ns = r.identifier.get.namespace().mkString(".")
+ val name = r.identifier.get.name()
+ tables += s"$cat.$ns.$name"
Review Comment:
Inconsistent name forms across the four match arms:
- `View` / `HiveTableRelation` / `LogicalRelation`: use
`TableIdentifier.unquotedString`, which is `db.tbl` when `catalog` is `None`
and `cat.db.tbl` when set.
- `DataSourceV2Relation`: `s"$cat.$ns.$name"` with `ns =
namespace().mkString(".")` — N+2 parts for an N-level namespace.
So the same metric view yields 2-, 3-, or 4+-part dependency names depending
on what kind of source was referenced. This ties into the `TableDependency`
Javadoc/contract issue — once `nameParts` is structural, all four arms can pass
`Seq[String]` directly without hand-formatting.
##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/ViewInfo.java:
##########
@@ -57,11 +58,14 @@ private 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;
+ // Force PROP_TABLE_TYPE = VIEW by default so that `properties()` reflects
the typed
+ // ViewInfo classification. Metric views intentionally set PROP_TABLE_TYPE
= METRIC_VIEW
+ // before construction and must retain that more-specific view kind.
+ String tableType = properties().get(TableCatalog.PROP_TABLE_TYPE);
+ if (!TableSummary.METRIC_VIEW_TABLE_TYPE.equals(tableType)) {
+ properties().put(TableCatalog.PROP_TABLE_TYPE,
TableSummary.VIEW_TABLE_TYPE);
+ }
Review Comment:
Embedding the metric-view-specific value in the generic `ViewInfo`
constructor is awkward — it makes `ViewInfo` aware of one specific
`PROP_TABLE_TYPE` value that lives in the metric-view code.
`BaseBuilder.withTableType` already exists (`TableInfo.java:147`); the
metric-view path could just call it after `withProperties(...)` and let this
constructor remain the unconditional default:
```java
private ViewInfo(Builder builder) {
super(builder);
...
properties().putIfAbsent(TableCatalog.PROP_TABLE_TYPE,
TableSummary.VIEW_TABLE_TYPE);
}
```
and in `metricViewCommands.scala` drop the `viewProperties.put(...)` for
`PROP_TABLE_TYPE` and add `.withTableType(TableSummary.METRIC_VIEW_TABLE_TYPE)`
after `.withProperties(viewProperties)`. That way `ViewInfo` doesn't reference
`METRIC_VIEW_TABLE_TYPE` at all, and adding future view kinds doesn't require
touching this class.
##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/TableDependency.java:
##########
@@ -0,0 +1,38 @@
+/*
+ * 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.Objects;
+
+import org.apache.spark.annotation.Evolving;
+
+/**
+ * A table dependency of a SQL object.
+ * <p>
+ * The dependent table is identified by its fully-qualified three-part name
+ * in the form {@code catalog_name.schema_name.table_name}.
+ *
+ * @param tableFullName fully-qualified three-part table name
Review Comment:
The "three-part name in the form `catalog_name.schema_name.table_name`"
claim doesn't hold for catalogs with multi-level namespaces (e.g. Iceberg
`cat.db1.db2.tbl` is 4 parts), and the producer in
`MetricViewHelper.collectTableDependencies` doesn't always emit three parts
either: V1 sources use `TableIdentifier.unquotedString` which yields 2 parts
when the catalog component is unset, and V2 sources dot-join the namespace
which yields N parts.
Since the dot-flattened string is also ambiguous against quoted identifiers
containing a literal `.`, the cleanest fix is to make this DTO structural —
e.g. `record TableDependency(String[] nameParts)` (and the same for
`FunctionDependency`). Consumers can join the parts themselves if they need a
flat string, but they can't recover arity if the join happens at storage time.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/metricview/serde/MetricViewCanonical.scala:
##########
@@ -167,4 +167,33 @@ private[sql] case class MetricView(
version: String,
from: Source,
where: Option[String] = None,
- select: Seq[Column])
+ select: Seq[Column]) {
+
+ /**
+ * Returns a set of table properties describing this metric view's source and
+ * filter clauses. Mirrors the property keys used by the canonical metric
view
+ * representation on other Spark platforms so consumers of the catalog see a
+ * consistent property layout.
+ */
+ def getProperties: Map[String, String] = {
Review Comment:
Worth noting in the Scaladoc that `metric_view.from.sql` is truncated to
`Constants.MAXIMUM_PROPERTY_SIZE` and is therefore a descriptive value (for
catalog browsers / lineage tooling), not a round-trippable representation of
the source — a consumer relying on it to reconstruct the view body would
silently get a truncated query for any view whose source SQL exceeds the limit.
##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/Dependency.java:
##########
@@ -0,0 +1,39 @@
+/*
+ * 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}.
+ *
+ * @since 4.2.0
+ */
+@Evolving
+public interface Dependency {
Review Comment:
The Javadoc claim "A dependency is one of: TableDependency or
FunctionDependency" is only structurally enforced if the interface is sealed.
As a plain `interface` on Java 17, third-party connectors can implement it and
the "is one of" claim is no longer true.
```suggestion
@Evolving
public sealed interface Dependency permits TableDependency,
FunctionDependency {
```
(Or alternatively, drop the "is one of" line if you want to leave the
interface open for future extension.)
##########
sql/catalyst/src/main/java/org/apache/spark/sql/connector/catalog/DependencyList.java:
##########
@@ -0,0 +1,46 @@
+/*
+ * 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.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>
+ *
+ * @param dependencies array of dependencies
+ * @since 4.2.0
+ */
+@Evolving
+public record DependencyList(Dependency[] dependencies) {
+
+ public DependencyList {
+ Objects.requireNonNull(dependencies, "dependencies must not be null");
Review Comment:
Records auto-generate accessors that return field references, so
`DependencyList.dependencies()` hands callers the live array — a downstream
consumer can mutate the lineage list of any `ViewInfo` via this accessor.
Either defensively copy in the canonical constructor and the accessor, or
expose `List<Dependency>` instead:
```java
public DependencyList {
Objects.requireNonNull(dependencies, "dependencies must not be null");
dependencies = dependencies.clone();
}
public Dependency[] dependencies() {
return dependencies.clone();
}
```
Same consideration applies to existing `ViewInfo.queryColumnNames()` /
`currentNamespace()`, but those are pre-existing — at least worth doing it on
the new types.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]