@@ -25,7 +25,6 @@ import org.apache.spark.internal.Logging
2525import org .apache .spark .sql .AnalysisException
2626import org .apache .spark .sql .catalyst .FunctionIdentifier
2727import org .apache .spark .sql .catalyst .analysis .FunctionRegistry
28- import org .apache .spark .sql .catalyst .catalog .SessionCatalog
2928import org .apache .spark .sql .catalyst .expressions ._
3029import org .apache .spark .sql .catalyst .expressions .aggregate ._
3130import org .apache .spark .sql .catalyst .plans .logical .LogicalPlan
@@ -69,6 +68,13 @@ class FunctionResolution(
6968
7069 private val trimWarningEnabled = new AtomicBoolean (true )
7170
71+ /** Returns the current catalog path, preferring the view's context if resolving a view. */
72+ private def currentCatalogPath : Seq [String ] = {
73+ val ctx = AnalysisContext .get.catalogAndNamespace
74+ if (ctx.nonEmpty) ctx
75+ else (Seq (catalogManager.currentCatalog.name) ++ catalogManager.currentNamespace).toSeq
76+ }
77+
7278 /**
7379 * Produces the ordered list of fully qualified candidate names for resolution.
7480 *
@@ -77,12 +83,7 @@ class FunctionResolution(
7783 */
7884 private def resolutionCandidates (nameParts : Seq [String ]): Seq [Seq [String ]] = {
7985 if (nameParts.size == 1 ) {
80- val catalogPath = if (AnalysisContext .get.catalogAndNamespace.nonEmpty) {
81- AnalysisContext .get.catalogAndNamespace
82- } else {
83- (Seq (catalogManager.currentCatalog.name) ++ catalogManager.currentNamespace).toSeq
84- }
85- val searchPath = SQLConf .get.resolutionSearchPath(catalogPath)
86+ val searchPath = SQLConf .get.resolutionSearchPath(currentCatalogPath)
8687 searchPath.map(_ ++ nameParts)
8788 } else {
8889 nameParts.size match {
@@ -100,7 +101,7 @@ class FunctionResolution(
100101 unresolvedFunc : UnresolvedFunction ): Option [Expression ] = {
101102 if (nameParts.length == 3 &&
102103 nameParts.head.equalsIgnoreCase(CatalogManager .SYSTEM_CATALOG_NAME )) {
103- // Try resolving as a session-namespace function (builtin, temp, extension )
104+ // Try resolving as a session-namespace function (builtin or temp )
104105 FunctionResolution .sessionNamespaceKind(nameParts).flatMap { kind =>
105106 val funcName = nameParts.last
106107 val expr = v1SessionCatalog.resolveScalarFunction(kind, funcName, unresolvedFunc.arguments)
@@ -126,16 +127,6 @@ class FunctionResolution(
126127 resolveV2Function(unboundV2Func, unresolvedFunc.arguments, unresolvedFunc)
127128 })
128129 } catch {
129- case e : AnalysisException if e.getCondition == " REQUIRES_SINGLE_PART_NAMESPACE" &&
130- nameParts.size == 3 =>
131- val catalogPath = if (AnalysisContext .get.catalogAndNamespace.nonEmpty) {
132- AnalysisContext .get.catalogAndNamespace
133- } else {
134- (Seq (catalogManager.currentCatalog.name) ++ catalogManager.currentNamespace).toSeq
135- }
136- val searchPath = SQLConf .get.resolutionSearchPath(catalogPath)
137- throw QueryCompilationErrors .unresolvedRoutineError(
138- unresolvedFunc.nameParts, searchPath.map(toSQLId), unresolvedFunc.origin)
139130 case _ : NoSuchFunctionException =>
140131 None
141132 case _ : NoSuchNamespaceException =>
@@ -176,12 +167,7 @@ class FunctionResolution(
176167 case None =>
177168 }
178169 }
179- val catalogPath = if (AnalysisContext .get.catalogAndNamespace.nonEmpty) {
180- AnalysisContext .get.catalogAndNamespace
181- } else {
182- (Seq (catalogManager.currentCatalog.name) ++ catalogManager.currentNamespace).toSeq
183- }
184- val searchPath = SQLConf .get.resolutionSearchPath(catalogPath)
170+ val searchPath = SQLConf .get.resolutionSearchPath(currentCatalogPath)
185171 throw QueryCompilationErrors .unresolvedRoutineError(
186172 unresolvedFunc.nameParts, searchPath.map(toSQLId), unresolvedFunc.origin)
187173 }
@@ -300,46 +286,19 @@ class FunctionResolution(
300286 /**
301287 * Determines the type/location of a function (builtin, temporary, persistent, etc.).
302288 * This is used by the LookupFunctions analyzer rule for early validation and optimization.
303- * This method only performs the lookup and classification - it does not throw errors.
289+ *
290+ * Note: may throw for malformed identifiers (e.g. REQUIRES_SINGLE_PART_NAMESPACE).
304291 *
305292 * @param nameParts The function name parts.
306293 * @param unresolvedFunc Optional UnresolvedFunction node for lookups that may need it.
307- * @return The type of the function (Builtin, Temporary , Persistent, TableOnly, or NotFound).
294+ * @return The type of the function (Local , Persistent, TableOnly, or NotFound).
308295 */
309296 def lookupFunctionType (
310297 nameParts : Seq [String ],
311298 unresolvedFunc : Option [UnresolvedFunction ] = None ): FunctionType = {
312299
313- // Check if it's explicitly qualified as extension, builtin, or temp
314- FunctionResolution .sessionNamespaceKind(nameParts) match {
315- case Some (SessionCatalog .Extension ) | Some (SessionCatalog .Builtin ) =>
316- if (lookupBuiltinOrTempFunction(nameParts, unresolvedFunc).isDefined) {
317- return FunctionType .Local // Extension and builtin both as Local
318- }
319- case Some (SessionCatalog .Temp ) =>
320- if (lookupBuiltinOrTempFunction(nameParts, unresolvedFunc).isDefined) {
321- return FunctionType .Local
322- }
323- case None =>
324- // Unqualified or qualified with a catalog
325- // Use lookupBuiltinOrTempFunction which handles internal functions correctly
326- val funcInfoOpt = lookupBuiltinOrTempFunction(nameParts, unresolvedFunc)
327- funcInfoOpt match {
328- case Some (info) =>
329- // Determine if it's extension, temp, or builtin from the ExpressionInfo
330- if (info.getDb == CatalogManager .EXTENSION_NAMESPACE ) {
331- return FunctionType .Local
332- } else if (info.getDb == CatalogManager .SESSION_NAMESPACE ) {
333- if (nameParts.size == 1 && unresolvedFunc.exists(_.isInternal)) {
334- return FunctionType .Local
335- } else {
336- return FunctionType .Local
337- }
338- } else {
339- return FunctionType .Local
340- }
341- case None =>
342- }
300+ if (lookupBuiltinOrTempFunction(nameParts, unresolvedFunc).isDefined) {
301+ return FunctionType .Local
343302 }
344303
345304 // Check if function exists as table function only
@@ -601,46 +560,33 @@ class FunctionResolution(
601560 * Companion object with shared utility methods for function name qualification checks.
602561 */
603562object FunctionResolution {
604- /**
605- * Check if a function name is qualified as an extension function.
606- * Valid forms: extension.func or system.extension.func
607- */
608- private def maybeExtensionFunctionName (nameParts : Seq [String ]): Boolean = {
609- isQualifiedWithNamespace(nameParts, CatalogManager .EXTENSION_NAMESPACE )
610- }
611-
612563 /**
613564 * Check if a function name is qualified as a builtin function.
614565 * Valid forms: builtin.func or system.builtin.func
615566 */
616567 private def maybeBuiltinFunctionName (nameParts : Seq [String ]): Boolean = {
617- isQualifiedWithNamespace (nameParts, CatalogManager .BUILTIN_NAMESPACE )
568+ isQualifiedWithSystemNamespace (nameParts, CatalogManager .BUILTIN_NAMESPACE )
618569 }
619570
620571 /**
621572 * Check if a function name is qualified as a session temporary function.
622573 * Valid forms: session.func or system.session.func
623574 */
624575 private def maybeTempFunctionName (nameParts : Seq [String ]): Boolean = {
625- isQualifiedWithNamespace (nameParts, CatalogManager .SESSION_NAMESPACE )
576+ isQualifiedWithSystemNamespace (nameParts, CatalogManager .SESSION_NAMESPACE )
626577 }
627578
628579 /**
629580 * Single qualification result for session namespaces: returns the kind when nameParts
630- * is explicitly qualified as extension, builtin, or session; None otherwise.
631- * Use this instead of calling maybeBuiltinFunctionName/maybeTempFunctionName/
632- * maybeExtensionFunctionName in multiple places so namespace rules live in one place.
581+ * is explicitly qualified as builtin or session; None otherwise.
633582 *
634583 * @param nameParts The function name parts (e.g. Seq("builtin", "abs"), Seq("session", "my_udf"))
635- * @return Some(Builtin), Some(Temp), or Some(Extension) for 2/3-part session qualification;
636- * None otherwise
584+ * @return Some(Builtin) or Some(Temp) for 2/3-part session qualification; None otherwise
637585 */
638586 def sessionNamespaceKind (nameParts : Seq [String ])
639587 : Option [org.apache.spark.sql.catalyst.catalog.SessionCatalog .SessionFunctionKind ] = {
640588 if (nameParts.length <= 1 ) None
641- else if (maybeExtensionFunctionName(nameParts)) {
642- Some (org.apache.spark.sql.catalyst.catalog.SessionCatalog .Extension )
643- } else if (maybeBuiltinFunctionName(nameParts)) {
589+ else if (maybeBuiltinFunctionName(nameParts)) {
644590 Some (org.apache.spark.sql.catalyst.catalog.SessionCatalog .Builtin )
645591 } else if (maybeTempFunctionName(nameParts)) {
646592 Some (org.apache.spark.sql.catalyst.catalog.SessionCatalog .Temp )
@@ -653,10 +599,10 @@ object FunctionResolution {
653599 * Validates both the namespace prefix AND that a function name is present.
654600 *
655601 * @param nameParts The multi-part name to check
656- * @param namespace The namespace to check for (e.g., "extension", " builtin", "session")
602+ * @param namespace The namespace to check for (e.g., "builtin", "session")
657603 * @return true if qualified with the given namespace and has a non-empty function name
658604 */
659- private def isQualifiedWithNamespace (nameParts : Seq [String ], namespace : String ): Boolean = {
605+ private def isQualifiedWithSystemNamespace (nameParts : Seq [String ], namespace : String ): Boolean = {
660606 nameParts.length match {
661607 case 2 =>
662608 // Format: namespace.funcName (e.g., "builtin.abs")
0 commit comments