cloud-fan commented on code in PR #60076:
URL: https://github.com/apache/spark/pull/60076#discussion_r4483410046


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/ColumnResolutionHelper.scala:
##########
@@ -473,6 +485,104 @@ trait ColumnResolutionHelper extends Logging with 
DataTypeErrorsBase {
       includeLastResort = includeLastResort)
   }
 
+  /** Hides helper grouping outputs when missing-column resolution searches an 
Aggregate. */
+  private def resolveOutputWithoutHavingHelpers(
+      plan: LogicalPlan,
+      nameParts: Seq[String]): Option[NamedExpression] = {
+    if 
(conf.getConf(SQLConf.HAVING_WITH_WINDOW_RESOLVES_GROUPING_COLUMN_FIRST)) {
+      resolveWithoutHavingHelpers(nameParts, plan.output, 
plan.resolve(nameParts, conf.resolver))
+    } else {
+      plan.resolve(nameParts, conf.resolver)
+    }
+  }
+
+  /** Keeps helper grouping outputs out of ordinary SELECT-alias resolution. */
+  private def resolveChildrenWithHavingScope(
+      plan: LogicalPlan,
+      nameParts: Seq[String]): Option[NamedExpression] = {
+    val hasHavingScope =
+      
plan.getTagValue(ColumnResolutionHelper.HAVING_GROUPING_SCOPE_TAG).contains(true)
 ||
+        (plan match {
+          // QUALIFY searches below the projection that trims HAVING helper 
outputs.
+          case project: Project => project.child.getTagValue(
+            ColumnResolutionHelper.HAVING_GROUPING_SCOPE_TAG).contains(true)
+          case _ => false
+        })
+    if (hasHavingScope) {
+      resolveWithoutHavingHelpers(
+        nameParts, plan.children.flatMap(_.output), 
plan.resolveChildren(nameParts, conf.resolver))
+    } else {
+      plan.resolveChildren(nameParts, conf.resolver)
+    }
+  }
+
+  /** Keeps ordinary lookup indexed and hides HAVING helpers in resolved 
results and errors. */
+  private def resolveWithoutHavingHelpers(
+      nameParts: Seq[String],
+      output: => Seq[Attribute],
+      resolve: => Option[NamedExpression]): Option[NamedExpression] = {
+    lazy val allOutput = output
+    lazy val visibleOutput = allOutput.filterNot(
+      
_.metadata.contains(ColumnResolutionHelper.HAVING_GROUPING_COLUMN_METADATA_KEY))
+    val resolved = try {
+      resolve
+    } catch {
+      // Helpers can cause ambiguity or a nested-field error before lookup 
returns a candidate.
+      case _: AnalysisException if visibleOutput.length != allOutput.length =>
+        return LocalRelation(visibleOutput).resolve(nameParts, conf.resolver)
+    }
+    val mayReferenceHelpers = resolved.exists { expression =>
+      val references = expression.references
+      references.exists(
+        
_.metadata.contains(ColumnResolutionHelper.HAVING_GROUPING_COLUMN_METADATA_KEY))
 ||
+        // NULL field extraction can discard the helper reference during 
resolution.
+        (nameParts.length > 1 && references.isEmpty &&
+          visibleOutput.length != allOutput.length)
+    }
+    if (mayReferenceHelpers) {
+      LocalRelation(visibleOutput).resolve(nameParts, conf.resolver)
+    } else {
+      resolved
+    }
+  }
+
+  /**
+   * Resolves complete grouping expressions before SELECT aliases in extracted 
HAVING filters.
+   * Resolve against the full input to retain ambiguity, and only accept 
actual grouping
+   * expressions so ungrouped inputs cannot mask aliases or influence their 
type coercion.
+   */
+  private def resolveHavingGroupingExpressions(
+      expression: Expression,
+      plan: LogicalPlan,
+      wrap: Expression => Expression = identity): Expression = {
+    if 
(!plan.getTagValue(ColumnResolutionHelper.HAVING_GROUPING_SCOPE_TAG).contains(true))
 {
+      return expression
+    }
+    val aggregate = plan.collectFirst { case aggregate: Aggregate => aggregate 
}
+    aggregate.map { aggregate =>
+      // Leave lambda parameters and captures alone until the lambda has been 
bound.
+      def resolveGrouping(expression: Expression): Expression = expression 
match {
+        case _ if expression.resolved => expression
+        case lambda: LambdaFunction if !lambda.bound => lambda
+        case _ =>
+          val candidate = resolveExpressionByPlanOutput(expression, 
aggregate.child) match {
+            case Alias(child, _) => child
+            case other => other
+          }
+          val grouping = if (candidate.resolved &&
+              
aggregate.groupingExpressions.exists(_.semanticEquals(candidate))) {
+            // Keep inputs until aggregate extraction. UpdateOuterReferences 
then maps
+            // complete correlated grouping expressions to their real 
aggregate outputs.
+            Some(candidate)

Review Comment:
   **Blocking (P1):** With 
`spark.sql.analyzer.havingWithWindowResolvesGroupingColumnFirst.enabled=true`, 
this line binds a HAVING reference to a grouping key directly to the 
Aggregate's input attribute (`k#1`). When the key isn't selected as-is, the 
Aggregate outputs only the helper alias. So the Filter counts as resolved but 
references an attribute its child doesn't produce until 
ResolveAggregateFunctions remaps it. `preserveHavingScope` also stops 
ResolveReferences from adding `k#1` to the Aggregate.
   
   ResolveSQLFunctions runs between those two rules (Analyzer.scala:708 vs. 
724). Its Filter-over-Aggregate deferral (3023-3026) only recognizes unresolved 
filters, aggregates, grouping functions and `TempResolvedColumn`, so it 
rewrites this Filter or its Aggregate and puts a Project between them. After 
that, the `Filter(cond, agg: Aggregate)` remap never matches, and CheckAnalysis 
fails with missing attributes.
   
   Here are two examples, using `CREATE TEMPORARY FUNCTION having_id(v INT) 
RETURNS INT RETURN v`. By my reading of the rules, both fail analysis with the 
conf on. On the base branch and with the conf off, both return `(20, 1)`:
   ```sql
   SELECT sum(v) AS s, count(*) OVER () AS n FROM VALUES (1, 10), (2, 20) AS 
t(k, v)
   GROUP BY k HAVING having_id(k) > 1;
   SELECT having_id(sum(v)) AS s, count(*) OVER () AS n FROM VALUES (1, 10), 
(2, 20) AS t(k, v)
   GROUP BY k HAVING k > 1;
   ```
   On the base branch, `k` resolves to a `TempResolvedColumn`, so the 
SQL-function rewrite waits until the grouping column is in the Aggregate 
output. The new goldens use a SQL UDF only inside a FROM subquery, so they 
don't cover this.
   
   See **Shared repair plan 1** in the review body.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala:
##########
@@ -4838,7 +4889,14 @@ object UpdateOuterReferences extends Rule[LogicalPlan] {
             // Collect the aliases from output of aggregate.
             val outerAliases = a.aggregateExpressions collect { case a: Alias 
=> a }
             // Update the subquery plan to record the OuterReference to point 
to outer query plan.
-            s.withNewPlan(updateOuterReferenceInSubquery(s.plan, outerAliases))
+            val newPlan = updateOuterReferenceInSubquery(s.plan, outerAliases)
+            val newSubquery = s.withNewPlan(newPlan)
+            if 
(f.getTagValue(ColumnResolutionHelper.HAVING_GROUPING_SCOPE_TAG).contains(true))
 {

Review Comment:
   **Non-blocking (P2):** This refresh only fires while the HAVING Filter still 
carries `HAVING_GROUPING_SCOPE_TAG` and still sits directly over its Aggregate. 
Two Resolution-batch rules break that after the subquery is resolved:
   - `ResolveQualify.resolveQualifyCondition` always rebuilds the nested Filter 
with `f.copy(child = newChild)` (Analyzer.scala:4608-4610). Only the rule's 
result root gets `copyTagsFrom`, so the HAVING Filter loses the tag.
   - When HAVING contains a SQL UDF, the `ResolveSQLFunctions` Filter case 
(3092-3097) rebuilds an untagged Filter over a Project, so `Filter(_, a: 
Aggregate)` no longer matches here at all.
   
   ResolveAggregateFunctions doesn't fix the outer attributes before that, 
because for a computed key the recorded input (`g#1`) is not itself a grouping 
expression. The Filter therefore keeps referencing `g#1`, which the Aggregate 
doesn't output, and CheckAnalysis fails. By my reading of the rules, this fails 
analysis with the conf on:
   ```sql
   SELECT (g + 1) + 10 AS g, count(*) OVER () AS n
   FROM VALUES (1), (2) AS t(g)
   GROUP BY g + 1 HAVING (SELECT g + 1) >= 3 QUALIFY n > 0;
   ```
   Without `QUALIFY n > 0` it returns `(13, 1)`, as the new golden shows. 
Replacing the QUALIFY with a `having_id(1) > 0 AND` term in HAVING fails the 
same way.
   
   See **Shared repair plan 1** in the review body.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala:
##########
@@ -3883,12 +3932,14 @@ class Analyzer(
             throw 
QueryCompilationErrors.lateralColumnAliasInAggWithWindowAndHavingUnsupportedError(
               lcaRef.nameParts)
         })
-        val (windowExpressions, aggregateExpressions) = 
extract(aggregateExprs, groupingExprs)
-        // Create an Aggregate operator to evaluate aggregation functions.
-        val withAggregate = Aggregate(groupingExprs, aggregateExpressions, 
child)
-        // Add a Filter operator for conditions in the Having clause.
-        val withFilter = Filter(condition, withAggregate)
-        val withWindow = addWindow(windowExpressions, withFilter)
+        val withWindow = if (
+            
conf.getConf(SQLConf.HAVING_WITH_WINDOW_RESOLVES_GROUPING_COLUMN_FIRST)) {
+          addWindowsWithHaving(groupingExprs, aggregateExprs, condition, child)

Review Comment:
   **Non-blocking (P2):** This is the only place where the conf moves HAVING 
below every window. Two unchanged producers that build the HAVING Filter 
themselves don't get the fix:
   - GROUPING SETS / ROLLUP / CUBE: when the condition already resolves in 
`ResolveGroupingAnalytics.tryResolveHavingCondition` (Analyzer.scala:936-987), 
for example a grouping column compared with a literal of the same type, that 
rule returns `Filter(newCond, newChild)` over the window-bearing expanded 
Aggregate. ExtractWindowExpressions' Aggregate case then extracts the windows 
below that Filter.
   - Generator queries: `ResolveUnresolvedHaving.insertFilterBeforeWindowChain` 
(4977-4991) only recurses through directly stacked Windows, so the Project 
between nested windows leaves the Filter above the inner window.
   
   By my reading of these rules, with the conf on:
   - `SELECT k, count(*) OVER () AS n FROM VALUES (1, 10), (2, 20), (3, 30) AS 
t(k, v) GROUP BY GROUPING SETS ((k)) HAVING k = 2` still computes n=3 instead 
of 1.
   - The new nested-window golden query with `GROUP BY ROLLUP(k)` computes n=10 
instead of 1.
   - The same query with `explode(array(k)) AS col` added to the SELECT list 
computes n=2 instead of 1.
   
   This extends an [existing 
discussion](https://github.com/apache/spark/pull/60076#issuecomment-6633654035).
   
   **Recommended change:** With the conf on, place HAVING below every window on 
the remaining producers. When the grouping-sets Aggregate carries window 
functions, ResolveGroupingAnalytics should leave the HAVING clause unresolved 
over the expanded Aggregate, as it already does when the condition cannot 
resolve yet, so window extraction places it with the conf-gated logic. The 
generator path's HAVING placement should continue through the Project that sits 
between nested windows while the condition's columns are still available below 
it. Add HAVING golden coverage for grouping-sets and generator queries with 
windows under both conf values.
   
   **Why this works:** Both producers then hand placement to the logic that 
already filters groups before every window. Grouping-sets queries reach the 
same UnresolvedHaving-over-Aggregate shape that their other sub-path already 
produces. Generator queries walk through the extraction Projects to the 
innermost window, guarded by the existing reference check.
   
   **Scope:** Route grouping-sets HAVING with windows, and generator HAVING 
with nested windows, to below-every-window placement when the conf is on.
   
   **Compatibility:** With the conf off, every producer keeps its base-branch 
placement, including the unconditional SPARK-58503 behavior for directly 
stacked windows in generator queries.
   
   **Risks:** With the conf on, grouping-sets HAVING conditions that used to 
resolve inside ResolveGroupingAnalytics now resolve through the HAVING-scope 
lookup and helpers. Results for grouping keys must agree, since both paths are 
grouping-first, and grouping()/grouping_id() rewriting must still apply to the 
moved Filter. Turning the conf off rolls this back. Pushing the generator 
path's Filter through extraction Projects changes those plans with the conf on. 
The existing reference check must keep it from crossing a Project that does not 
pass the condition's columns through.
   
   **Constraints:** Gate both changes on 
spark.sql.analyzer.havingWithWindowResolvesGroupingColumnFirst.enabled so that 
the conf-off plans match the base branch. Keep the unconditional SPARK-58503 
placement for directly stacked windows unchanged.
   
   **Success:** With the conf on, `SELECT k, count(*) OVER () AS n FROM VALUES 
(1, 10), (2, 20), (3, 30) AS t(k, v) GROUP BY GROUPING SETS ((k)) HAVING k = 2` 
returns n=1, and the ROLLUP query with `sum(rank() OVER (ORDER BY sum(v))) OVER 
()` and `HAVING k = 2` returns 1. With the conf on, `SELECT explode(array(k)) 
AS col, k, sum(rank() OVER (ORDER BY sum(v))) OVER () AS n FROM VALUES (1, 10), 
(2, 20), (3, 30) AS t(k, v) GROUP BY k HAVING k = 2` returns n=1. With the conf 
off, these queries keep their base-branch results (n=3, 10 and 2).
   
   **Verification:**
   - **Regression:** With the conf on, GROUPING SETS, ROLLUP and CUBE queries 
with single and nested window functions whose HAVING condition compares a 
grouping column with a same-typed literal compute their windows only over the 
groups that HAVING keeps.
   - **Regression:** With the conf on, a generator query with nested window 
functions and HAVING computes the inner window only over the groups that HAVING 
keeps.
   - **Compatibility:** With the conf off, the same grouping-sets and generator 
queries keep the base branch's plans and results, and directly stacked 
generator windows keep the SPARK-58503 placement.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/ColumnResolutionHelper.scala:
##########
@@ -473,6 +485,104 @@ trait ColumnResolutionHelper extends Logging with 
DataTypeErrorsBase {
       includeLastResort = includeLastResort)
   }
 
+  /** Hides helper grouping outputs when missing-column resolution searches an 
Aggregate. */
+  private def resolveOutputWithoutHavingHelpers(
+      plan: LogicalPlan,
+      nameParts: Seq[String]): Option[NamedExpression] = {
+    if 
(conf.getConf(SQLConf.HAVING_WITH_WINDOW_RESOLVES_GROUPING_COLUMN_FIRST)) {
+      resolveWithoutHavingHelpers(nameParts, plan.output, 
plan.resolve(nameParts, conf.resolver))
+    } else {
+      plan.resolve(nameParts, conf.resolver)
+    }
+  }
+
+  /** Keeps helper grouping outputs out of ordinary SELECT-alias resolution. */
+  private def resolveChildrenWithHavingScope(
+      plan: LogicalPlan,
+      nameParts: Seq[String]): Option[NamedExpression] = {
+    val hasHavingScope =
+      
plan.getTagValue(ColumnResolutionHelper.HAVING_GROUPING_SCOPE_TAG).contains(true)
 ||
+        (plan match {
+          // QUALIFY searches below the projection that trims HAVING helper 
outputs.
+          case project: Project => project.child.getTagValue(
+            ColumnResolutionHelper.HAVING_GROUPING_SCOPE_TAG).contains(true)
+          case _ => false
+        })
+    if (hasHavingScope) {
+      resolveWithoutHavingHelpers(
+        nameParts, plan.children.flatMap(_.output), 
plan.resolveChildren(nameParts, conf.resolver))
+    } else {
+      plan.resolveChildren(nameParts, conf.resolver)
+    }
+  }
+
+  /** Keeps ordinary lookup indexed and hides HAVING helpers in resolved 
results and errors. */
+  private def resolveWithoutHavingHelpers(
+      nameParts: Seq[String],
+      output: => Seq[Attribute],
+      resolve: => Option[NamedExpression]): Option[NamedExpression] = {
+    lazy val allOutput = output
+    lazy val visibleOutput = allOutput.filterNot(
+      
_.metadata.contains(ColumnResolutionHelper.HAVING_GROUPING_COLUMN_METADATA_KEY))
+    val resolved = try {
+      resolve
+    } catch {
+      // Helpers can cause ambiguity or a nested-field error before lookup 
returns a candidate.
+      case _: AnalysisException if visibleOutput.length != allOutput.length =>
+        return LocalRelation(visibleOutput).resolve(nameParts, conf.resolver)
+    }
+    val mayReferenceHelpers = resolved.exists { expression =>
+      val references = expression.references
+      references.exists(
+        
_.metadata.contains(ColumnResolutionHelper.HAVING_GROUPING_COLUMN_METADATA_KEY))
 ||
+        // NULL field extraction can discard the helper reference during 
resolution.
+        (nameParts.length > 1 && references.isEmpty &&
+          visibleOutput.length != allOutput.length)
+    }
+    if (mayReferenceHelpers) {
+      LocalRelation(visibleOutput).resolve(nameParts, conf.resolver)
+    } else {
+      resolved
+    }
+  }
+
+  /**
+   * Resolves complete grouping expressions before SELECT aliases in extracted 
HAVING filters.
+   * Resolve against the full input to retain ambiguity, and only accept 
actual grouping
+   * expressions so ungrouped inputs cannot mask aliases or influence their 
type coercion.
+   */
+  private def resolveHavingGroupingExpressions(
+      expression: Expression,
+      plan: LogicalPlan,
+      wrap: Expression => Expression = identity): Expression = {
+    if 
(!plan.getTagValue(ColumnResolutionHelper.HAVING_GROUPING_SCOPE_TAG).contains(true))
 {
+      return expression
+    }
+    val aggregate = plan.collectFirst { case aggregate: Aggregate => aggregate 
}
+    aggregate.map { aggregate =>
+      // Leave lambda parameters and captures alone until the lambda has been 
bound.
+      def resolveGrouping(expression: Expression): Expression = expression 
match {
+        case _ if expression.resolved => expression
+        case lambda: LambdaFunction if !lambda.bound => lambda
+        case _ =>
+          val candidate = resolveExpressionByPlanOutput(expression, 
aggregate.child) match {
+            case Alias(child, _) => child
+            case other => other
+          }
+          val grouping = if (candidate.resolved &&

Review Comment:
   **Non-blocking (P2):** `candidate.resolved` only holds here for grouping 
expressions that resolve by attribute lookup alone. For a key like 
`lower(name)`, `resolveExpressionByPlanOutput` resolves `name` but leaves 
`lower` as an unresolved function. The subtree therefore never matches the 
grouping key. The walk descends to the bare `name`, and 
`resolveChildrenWithHavingScope` binds it to a same-named SELECT alias. 
ResolveFunctions waits for resolved arguments, so the function is never 
compared with the grouping key first. Keys that need an implicit cast, such as 
`GROUP BY k + 1.5`, take the same fallback.
   
   By my reading of the code, this query returns `bob!`:
   ```sql
   SELECT concat(lower(name), '!') AS name
   FROM VALUES ('Bob'), ('bob'), ('Al') AS t(name)
   GROUP BY lower(name) HAVING lower(name) = 'bob';
   ```
   Adding `, count(*) OVER () AS n` makes it return no row, with the conf on as 
well as off. The operator key in `GROUP BY g + 1 HAVING g + 1 >= 3` is fixed. 
So with the conf on, function and cast keys still get the wrong groups that the 
PR fixes for other keys. The description scopes function and type normalization 
out, but this is the same failure on the same path.
   
   **Recommended change:** In the HAVING-scope grouping lookup, handle a HAVING 
subtree whose columns all resolve against the Aggregate input but which stays 
unresolved because of function calls or implicit casts. Resolve a detached copy 
of it over that input the way the analyzer would, including functions and 
implicit casts. Bind the subtree to the grouping expression only when that copy 
semantically equals one. Otherwise discard the copy and keep today's alias 
fallback. Add HAVING golden coverage for function-based and cast-requiring 
grouping keys under both conf values.
   
   **Why this works:** The tentative copy gives the lookup the same view of 
lower(name) or k + 1.5 that the Aggregate's own grouping expressions have. A 
complete match then binds through the existing grouping path, and 
ResolveAggregateFunctions later remaps it to the Aggregate output. A non-match 
leaves the original unresolved subtree unchanged, so alias binding and its type 
behavior stay as they are now.
   
   **Scope:** Let the HAVING-scope lookup recognize grouping keys that need 
function resolution or implicit casts, without changing alias fallback for 
anything else.
   
   **Compatibility:** Alias fallback for non-grouped names keeps its current 
binding and types, and analysis with the conf off is unchanged.
   
   **Risks:** Each candidate subtree that is unresolved only because of 
functions or casts now pays a tentative resolution over the Aggregate input. 
This analyzer-only cost applies only with the conf on and is bounded by the 
HAVING condition's size. The tentative resolution must have no side effects, 
for example on SQL function context or nested-resolution state, and its 
failures must leave the subtree untouched so that errors stay as they are today.
   
   **Constraints:** Accept only complete grouping-expression matches, and keep 
alias fallback and its types for every other name. Apply only to the HAVING 
Filter created under the conf, so analysis with the conf off is unchanged.
   
   **Success:** With the conf on, `SELECT concat(lower(name), '!') AS name, 
count(*) OVER () AS n FROM VALUES ('Bob'), ('bob'), ('Al') AS t(name) GROUP BY 
lower(name) HAVING lower(name) = 'bob'` returns the `bob!` row, as the same 
query without the window does. With the conf on, a grouping key whose HAVING 
spelling needs an implicit cast, such as `GROUP BY k + 1.5`, binds to the 
grouping key rather than a same-named alias. Names that do not form a complete 
grouping expression keep resolving to SELECT aliases with the alias types.
   
   **Verification:**
   - **Regression:** With the conf on, HAVING references to function-based 
grouping keys (such as lower and substr) and cast-requiring grouping keys, 
whose input column shares a name with a SELECT alias, keep the same groups as 
the equivalent query without a window function.
   - **Compatibility:** With the conf on, the existing alias-fallback, 
ambiguity and alias-type cases keep their results, and with the conf off the 
new cases keep the base branch's results.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -465,6 +465,17 @@ object SQLConf {
     .intConf
     .createWithDefault(100)
 
+  val HAVING_WITH_WINDOW_RESOLVES_GROUPING_COLUMN_FIRST =
+    
buildConf("spark.sql.analyzer.havingWithWindowResolvesGroupingColumnFirst.enabled")
+      .internal()
+      .withBindingPolicy(ConfigBindingPolicy.SESSION)
+      .doc("When true, preserve HAVING name-resolution scope during window 
extraction so that " +

Review Comment:
   **Nit (P3):** The doc only describes name precedence, but this flag also 
switches ExtractWindowExpressions to `addWindowsWithHaving` 
(Analyzer.scala:3935-3942). That path puts HAVING below every window, including 
the inner window of a nested window function, and it changes results even 
without a name collision. For `SELECT k, sum(rank() OVER (ORDER BY sum(v))) 
OVER () AS n FROM VALUES (1, 10), (2, 20), (3, 30) AS t(k, v) GROUP BY k HAVING 
k = 2`, the new golden returns n=1 with the flag on. With the flag off, the 
inner rank runs over all three groups. Could the doc say that true also filters 
groups before every window, and that false keeps the legacy placement as well?
   
   **Verification:**
   - **Inspection:** The conf's doc text names both the HAVING name-resolution 
precedence and the HAVING-below-every-window placement, for both values.



##########
sql/core/src/test/resources/sql-tests/inputs/having.sql:
##########
@@ -127,3 +127,165 @@ SELECT col2 FROM hav_outer GROUP BY col2 HAVING (SELECT 
max(col1)) > 1;
 SELECT col2 FROM hav_outer GROUP BY col2 HAVING (SELECT col1);
 
 DROP VIEW hav_outer;
+
+-- HAVING keeps grouping scope below a window; aggregate aliases keep their 
types.
+SET 
spark.sql.analyzer.havingWithWindowResolvesGroupingColumnFirst.enabled=true;

Review Comment:
   **Non-blocking (P2):** Every conf-on query in this block uses plain GROUP 
BY. GROUPING SETS / ROLLUP / CUBE queries with a window function also reach 
`addWindowsWithHaving` whenever their HAVING condition is still unresolved in 
`tryResolveHavingCondition`, for example because it calls an aggregate. On that 
path the Expand's virtual `spark_grouping_id` becomes a hidden helper unless 
`grouping_id()` is selected. ResolveGroupingAnalytics then rewrites 
`grouping()` / `grouping_id()` in HAVING and ORDER BY into references to that 
same name. I traced `... GROUP BY GROUPING SETS ((k), ()) HAVING sum(v) > 0 AND 
grouping_id() = 0`, with `grouping_id()` unselected, and found no failure. But 
nothing pins it: the existing grouping-sets + HAVING + window goldens in 
window-with-aggregates.sql run with the conf off.
   
   **Verification:**
   - **Behavior:** With the conf on, grouping-sets queries with windows and a 
HAVING condition on an aggregate, a grouping() predicate, or an ORDER BY 
grouping_id() resolve without exposing the hidden grouping-id helper, and 
return the rows the equivalent queries without windows produce.
   - **Compatibility:** With the conf off, the same queries return the base 
branch's results.



-- 
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]

Reply via email to