@@ -35,6 +35,7 @@ import org.apache.spark.sql.catalyst.expressions.SubExprUtils._
3535import org .apache .spark .sql .catalyst .expressions .aggregate ._
3636import org .apache .spark .sql .catalyst .expressions .objects ._
3737import org .apache .spark .sql .catalyst .optimizer .OptimizeUpdateFields
38+ import org .apache .spark .sql .catalyst .parser .{ParseException , ParserInterface }
3839import org .apache .spark .sql .catalyst .plans ._
3940import org .apache .spark .sql .catalyst .plans .logical ._
4041import org .apache .spark .sql .catalyst .rules ._
@@ -44,7 +45,7 @@ import org.apache.spark.sql.catalyst.trees.CurrentOrigin.withOrigin
4445import org .apache .spark .sql .catalyst .trees .TreePattern ._
4546import org .apache .spark .sql .catalyst .util .{toPrettySQL , CharVarcharUtils }
4647import org .apache .spark .sql .catalyst .util .ResolveDefaultColumns ._
47- import org .apache .spark .sql .connector .catalog .{View => _ , _ }
48+ import org .apache .spark .sql .connector .catalog .{View => V2View , _ }
4849import org .apache .spark .sql .connector .catalog .CatalogV2Implicits ._
4950import org .apache .spark .sql .connector .catalog .TableChange .{After , ColumnPosition }
5051import org .apache .spark .sql .connector .catalog .functions .{AggregateFunction => V2AggregateFunction , BoundFunction , ScalarFunction , UnboundFunction }
@@ -238,6 +239,11 @@ class Analyzer(override val catalogManager: CatalogManager)
238239 errorOnExceed = true ,
239240 maxIterationsSetting = SQLConf .ANALYZER_MAX_ITERATIONS .key)
240241
242+ /**
243+ * Override to provide additional rules for the "Substitution" batch.
244+ */
245+ val extendedSubstitutionRules : Seq [Rule [LogicalPlan ]] = Nil
246+
241247 /**
242248 * Override to provide additional rules for the "Resolution" batch.
243249 */
@@ -262,11 +268,12 @@ class Analyzer(override val catalogManager: CatalogManager)
262268 // However, when manipulating deeply nested schema, `UpdateFields` expression tree could be
263269 // very complex and make analysis impossible. Thus we need to optimize `UpdateFields` early
264270 // at the beginning of analysis.
265- OptimizeUpdateFields ,
266- CTESubstitution ,
267- WindowsSubstitution ,
268- EliminateUnions ,
269- SubstituteUnresolvedOrdinals ),
271+ OptimizeUpdateFields +:
272+ CTESubstitution +:
273+ WindowsSubstitution +:
274+ EliminateUnions +:
275+ SubstituteUnresolvedOrdinals +:
276+ extendedSubstitutionRules : _* ),
270277 Batch (" Disable Hints" , Once ,
271278 new ResolveHints .DisableHints ),
272279 Batch (" Hints" , fixedPoint,
@@ -447,6 +454,74 @@ class Analyzer(override val catalogManager: CatalogManager)
447454 }
448455 }
449456
457+ /**
458+ * Substitute persisted views in parsed plans with parsed view sql text.
459+ */
460+ case class ViewSubstitution (sqlParser : ParserInterface ) extends Rule [LogicalPlan ] {
461+
462+ def apply (plan : LogicalPlan ): LogicalPlan = plan.resolveOperatorsUp {
463+ case u @ UnresolvedRelation (nameParts, _, _) if v1SessionCatalog.isTempView(nameParts) =>
464+ u
465+ case u @ UnresolvedRelation (
466+ parts @ NonSessionCatalogAndIdentifier (catalog, ident), _, _) if ! isSQLOnFile(parts) =>
467+ CatalogV2Util .loadView(catalog, ident)
468+ .map(createViewRelation(parts.quoted, _))
469+ .getOrElse(u)
470+ }
471+
472+ private def isSQLOnFile (parts : Seq [String ]): Boolean = parts match {
473+ case Seq (_, path) if path.contains(" /" ) => true
474+ case _ => false
475+ }
476+
477+ private def createViewRelation (name : String , view : V2View ): LogicalPlan = {
478+ if (! catalogManager.isCatalogRegistered(view.currentCatalog)) {
479+ throw new AnalysisException (
480+ s " Invalid current catalog ' ${view.currentCatalog}' in view ' $name' " )
481+ }
482+
483+ val child = parseViewText(name, view.sql)
484+ val desc = V2ViewDescription (name, view)
485+ val qualifiedChild = desc.viewCatalogAndNamespace match {
486+ case Seq () =>
487+ // Views from Spark 2.2 or prior do not store catalog or namespace,
488+ // however its sql text should already be fully qualified.
489+ child
490+ case catalogAndNamespace =>
491+ // Substitute CTEs within the view before qualifying table identifiers
492+ qualifyTableIdentifiers(CTESubstitution .apply(child), catalogAndNamespace)
493+ }
494+
495+ // The relation is a view, so we wrap the relation by:
496+ // 1. Add a [[View]] operator over the relation to keep track of the view desc;
497+ // 2. Wrap the logical plan in a [[SubqueryAlias]] which tracks the name of the view.
498+ SubqueryAlias (name, View (desc, false , qualifiedChild))
499+ }
500+
501+ private def parseViewText (name : String , viewText : String ): LogicalPlan = {
502+ try {
503+ sqlParser.parsePlan(viewText)
504+ } catch {
505+ case _ : ParseException =>
506+ throw QueryCompilationErrors .invalidViewText(viewText, name)
507+ }
508+ }
509+
510+ /**
511+ * Qualify table identifiers with default catalog and namespace if necessary.
512+ */
513+ private def qualifyTableIdentifiers (
514+ child : LogicalPlan ,
515+ catalogAndNamespace : Seq [String ]): LogicalPlan =
516+ child transform {
517+ case u @ UnresolvedRelation (Seq (table), _, _) =>
518+ u.copy(multipartIdentifier = catalogAndNamespace :+ table)
519+ case u @ UnresolvedRelation (parts, _, _)
520+ if ! catalogManager.isCatalogRegistered(parts.head) =>
521+ u.copy(multipartIdentifier = catalogAndNamespace.head +: parts)
522+ }
523+ }
524+
450525 /**
451526 * Substitute child plan with WindowSpecDefinitions.
452527 */
@@ -1003,7 +1078,7 @@ class Analyzer(override val catalogManager: CatalogManager)
10031078 // The view's child should be a logical plan parsed from the `desc.viewText`, the variable
10041079 // `viewText` should be defined, or else we throw an error on the generation of the View
10051080 // operator.
1006- case view @ View (desc, isTempView, child) if ! child.resolved =>
1081+ case view @ View (CatalogTableViewDescription ( desc) , isTempView, child) if ! child.resolved =>
10071082 // Resolve all the UnresolvedRelations and Views in the child.
10081083 val newChild = AnalysisContext .withAnalysisContext(desc) {
10091084 val nestedViewDepth = AnalysisContext .get.nestedViewDepth
@@ -1055,8 +1130,9 @@ class Analyzer(override val catalogManager: CatalogManager)
10551130 write.table match {
10561131 case u : UnresolvedRelation if ! u.isStreaming =>
10571132 lookupRelation(u).map(unwrapRelationPlan).map {
1058- case v : View => throw QueryCompilationErrors .writeIntoViewNotAllowedError(
1059- v.desc.identifier, write)
1133+ case View (CatalogTableViewDescription (desc), _, _) =>
1134+ throw QueryCompilationErrors .writeIntoViewNotAllowedError(
1135+ desc.identifier, write)
10601136 case r : DataSourceV2Relation => write.withNewTable(r)
10611137 case u : UnresolvedCatalogRelation =>
10621138 throw QueryCompilationErrors .writeIntoV1TableNotAllowedError(
@@ -1139,20 +1215,32 @@ class Analyzer(override val catalogManager: CatalogManager)
11391215 }.orElse {
11401216 expandIdentifier(identifier) match {
11411217 case CatalogAndIdentifier (catalog, ident) =>
1142- CatalogV2Util .loadTable(catalog, ident).map {
1143- case v1Table : V1Table if CatalogV2Util .isSessionCatalog(catalog) &&
1144- v1Table.v1Table.tableType == CatalogTableType .VIEW =>
1145- val v1Ident = v1Table.catalogTable.identifier
1146- val v2Ident = Identifier .of(v1Ident.database.toArray, v1Ident.identifier)
1147- ResolvedView (v2Ident, isTemp = false )
1148- case table =>
1149- ResolvedTable .create(catalog.asTableCatalog, ident, table)
1150- }
1218+ lookupView(catalog, ident)
1219+ .orElse(lookupTable(catalog, ident))
11511220 case _ => None
11521221 }
11531222 }
11541223 }
11551224
1225+ private def lookupView (catalog : CatalogPlugin , ident : Identifier ): Option [LogicalPlan ] =
1226+ CatalogV2Util .loadView(catalog, ident).map {
1227+ case _ if CatalogV2Util .isSessionCatalog(catalog) =>
1228+ ResolvedView (ident, isTemp = false )
1229+ case view =>
1230+ ResolvedV2View (catalog.asViewCatalog, ident, view)
1231+ }
1232+
1233+ private def lookupTable (catalog : CatalogPlugin , ident : Identifier ): Option [LogicalPlan ] =
1234+ CatalogV2Util .loadTable(catalog, ident).map {
1235+ case v1Table : V1Table if CatalogV2Util .isSessionCatalog(catalog) &&
1236+ v1Table.v1Table.tableType == CatalogTableType .VIEW =>
1237+ val v1Ident = v1Table.catalogTable.identifier
1238+ val v2Ident = Identifier .of(v1Ident.database.toArray, v1Ident.identifier)
1239+ ResolvedView (v2Ident, isTemp = false )
1240+ case table =>
1241+ ResolvedTable .create(catalog.asTableCatalog, ident, table)
1242+ }
1243+
11561244 private def createRelation (
11571245 catalog : CatalogPlugin ,
11581246 ident : Identifier ,
0 commit comments