Skip to content

Commit af55373

Browse files
committed
[SPARK-34504][SQL] Avoid unnecessary resolving of SQL temp views for DDL commands
### What changes were proposed in this pull request? For DDL commands like DROP VIEW, they don't really need to resolve the view (parse and analyze the view SQL text), they just need to get the view metadata. This PR fixes the rule `ResolveTempViews` to only resolve the temp view for `UnresolvedRelation`. This also fixes a bug for DROP VIEW, as previously it tried to resolve the view and failed to drop invalid views. ### Why are the changes needed? bug fix ### Does this PR introduce _any_ user-facing change? no ### How was this patch tested? new test Closes apache#31853 from cloud-fan/view-resolve. Authored-by: Wenchen Fan <[email protected]> Signed-off-by: Wenchen Fan <[email protected]>
1 parent cef6650 commit af55373

File tree

2 files changed

+39
-21
lines changed

2 files changed

+39
-21
lines changed

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -899,24 +899,24 @@ class Analyzer(override val catalogManager: CatalogManager)
899899
object ResolveTempViews extends Rule[LogicalPlan] {
900900
def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperatorsUp {
901901
case u @ UnresolvedRelation(ident, _, isStreaming) =>
902-
lookupTempView(ident, isStreaming, performCheck = true).getOrElse(u)
902+
lookupAndResolveTempView(ident, isStreaming).getOrElse(u)
903903
case i @ InsertIntoStatement(UnresolvedRelation(ident, _, false), _, _, _, _, _) =>
904-
lookupTempView(ident, performCheck = true)
904+
lookupAndResolveTempView(ident)
905905
.map(view => i.copy(table = view))
906906
.getOrElse(i)
907907
case c @ CacheTable(UnresolvedRelation(ident, _, false), _, _, _) =>
908-
lookupTempView(ident, performCheck = true)
908+
lookupAndResolveTempView(ident)
909909
.map(view => c.copy(table = view))
910910
.getOrElse(c)
911911
case c @ UncacheTable(UnresolvedRelation(ident, _, false), _, _) =>
912-
lookupTempView(ident, performCheck = true)
912+
lookupAndResolveTempView(ident)
913913
.map(view => c.copy(table = view, isTempView = true))
914914
.getOrElse(c)
915915
// TODO (SPARK-27484): handle streaming write commands when we have them.
916916
case write: V2WriteCommand =>
917917
write.table match {
918918
case UnresolvedRelation(ident, _, false) =>
919-
lookupTempView(ident, performCheck = true).map(unwrapRelationPlan).map {
919+
lookupAndResolveTempView(ident).map(unwrapRelationPlan).map {
920920
case r: DataSourceV2Relation => write.withNewTable(r)
921921
case _ => throw QueryCompilationErrors.writeIntoTempViewNotAllowedError(ident.quoted)
922922
}.getOrElse(write)
@@ -948,10 +948,9 @@ class Analyzer(override val catalogManager: CatalogManager)
948948
.getOrElse(u)
949949
}
950950

951-
def lookupTempView(
951+
private def lookupTempView(
952952
identifier: Seq[String],
953-
isStreaming: Boolean = false,
954-
performCheck: Boolean = false): Option[LogicalPlan] = {
953+
isStreaming: Boolean = false): Option[LogicalPlan] = {
955954
// Permanent View can't refer to temp views, no need to lookup at all.
956955
if (isResolvingView && !isReferredTempViewName(identifier)) return None
957956

@@ -964,7 +963,13 @@ class Analyzer(override val catalogManager: CatalogManager)
964963
if (isStreaming && tmpView.nonEmpty && !tmpView.get.isStreaming) {
965964
throw QueryCompilationErrors.readNonStreamingTempViewError(identifier.quoted)
966965
}
967-
tmpView.map(ResolveRelations.resolveViews(_, performCheck))
966+
tmpView
967+
}
968+
969+
private def lookupAndResolveTempView(
970+
identifier: Seq[String],
971+
isStreaming: Boolean = false): Option[LogicalPlan] = {
972+
lookupTempView(identifier, isStreaming).map(ResolveRelations.resolveViews)
968973
}
969974
}
970975

@@ -1128,7 +1133,7 @@ class Analyzer(override val catalogManager: CatalogManager)
11281133
// look at `AnalysisContext.catalogAndNamespace` when resolving relations with single-part name.
11291134
// If `AnalysisContext.catalogAndNamespace` is non-empty, analyzer will expand single-part names
11301135
// with it, instead of current catalog and namespace.
1131-
def resolveViews(plan: LogicalPlan, performCheck: Boolean = false): LogicalPlan = plan match {
1136+
def resolveViews(plan: LogicalPlan): LogicalPlan = plan match {
11321137
// The view's child should be a logical plan parsed from the `desc.viewText`, the variable
11331138
// `viewText` should be defined, or else we throw an error on the generation of the View
11341139
// operator.
@@ -1147,16 +1152,10 @@ class Analyzer(override val catalogManager: CatalogManager)
11471152
}
11481153
// Fail the analysis eagerly because outside AnalysisContext, the unresolved operators
11491154
// inside a view maybe resolved incorrectly.
1150-
// But for commands like `DropViewCommand`, resolving view is unnecessary even though
1151-
// there is unresolved node. So use the `performCheck` flag to skip the analysis check
1152-
// for these commands.
1153-
// TODO(SPARK-34504): avoid unnecessary view resolving and remove the `performCheck` flag
1154-
if (performCheck) {
1155-
checkAnalysis(newChild)
1156-
}
1155+
checkAnalysis(newChild)
11571156
view.copy(child = newChild)
11581157
case p @ SubqueryAlias(_, view: View) =>
1159-
p.copy(child = resolveViews(view, performCheck))
1158+
p.copy(child = resolveViews(view))
11601159
case _ => plan
11611160
}
11621161

@@ -1179,14 +1178,14 @@ class Analyzer(override val catalogManager: CatalogManager)
11791178

11801179
case c @ CacheTable(u @ UnresolvedRelation(_, _, false), _, _, _) =>
11811180
lookupRelation(u.multipartIdentifier, u.options, false)
1182-
.map(resolveViews(_, performCheck = true))
1181+
.map(resolveViews)
11831182
.map(EliminateSubqueryAliases(_))
11841183
.map(relation => c.copy(table = relation))
11851184
.getOrElse(c)
11861185

11871186
case c @ UncacheTable(u @ UnresolvedRelation(_, _, false), _, _) =>
11881187
lookupRelation(u.multipartIdentifier, u.options, false)
1189-
.map(resolveViews(_, performCheck = true))
1188+
.map(resolveViews)
11901189
.map(EliminateSubqueryAliases(_))
11911190
.map(relation => c.copy(table = relation))
11921191
.getOrElse(c)
@@ -1212,7 +1211,7 @@ class Analyzer(override val catalogManager: CatalogManager)
12121211

12131212
case u: UnresolvedRelation =>
12141213
lookupRelation(u.multipartIdentifier, u.options, u.isStreaming)
1215-
.map(resolveViews(_, performCheck = true)).getOrElse(u)
1214+
.map(resolveViews).getOrElse(u)
12161215

12171216
case u @ UnresolvedTable(identifier, cmd, relationTypeMismatchHint) =>
12181217
lookupTableOrView(identifier).map {

sql/core/src/test/scala/org/apache/spark/sql/execution/SQLViewTestSuite.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,25 @@ abstract class SQLViewTestSuite extends QueryTest with SQLTestUtils {
308308
}
309309
}
310310
}
311+
312+
test("SPARK-34504: drop an invalid view") {
313+
withTable("t") {
314+
sql("CREATE TABLE t(s STRUCT<i: INT, j: INT>) USING json")
315+
val viewName = createView("v", "SELECT s.i FROM t")
316+
withView(viewName) {
317+
assert(spark.table(viewName).collect().isEmpty)
318+
319+
// re-create the table without nested field `i` which is referred by the view.
320+
sql("DROP TABLE t")
321+
sql("CREATE TABLE t(s STRUCT<j: INT>) USING json")
322+
val e = intercept[AnalysisException](spark.table(viewName))
323+
assert(e.message.contains("No such struct field i in j"))
324+
325+
// drop invalid view should be fine
326+
sql(s"DROP VIEW $viewName")
327+
}
328+
}
329+
}
311330
}
312331

313332
class LocalTempViewTestSuite extends SQLViewTestSuite with SharedSparkSession {

0 commit comments

Comments
 (0)