Bug: "Cannot use an UnspecifiedFrame" error for User Defined Aggregation Function over Window
Status: Open | Priority: Major | Resolution: 
Created: 05/Oct/22 21:35
Affects Version/s: 3.1.1

I have simplified reproducing the problem to the following. The original details for this bug description are below.
{code:java}
import org.apache.spark.sql.{Encoder, Encoders}
import org.apache.spark.sql.expressions.{Aggregator, Window}
import org.apache.spark.sql.functionsimport spark.implicits._

// A record that a user did something.
case class UserAction(who: Long, what: Long)

implicit val userActionEncoder: Encoder[UserAction] = Encoders.product[UserAction]

val top1 = new Aggregator[UserAction, UserAction, UserAction] {
  override def zero: UserAction = UserAction(-1L, -1L)
  override def reduce(b: UserAction, a: UserAction): UserAction = if (a.what > b.what) a else b
  override def merge(b1: UserAction, b2: UserAction): UserAction = reduce(b1, b2)
  override def finish(reduction: UserAction): UserAction = reduction
  override def bufferEncoder: Encoder[UserAction] = userActionEncoder
  override def outputEncoder: Encoder[UserAction] = userActionEncoder
}

val values = Seq(
  UserAction(1, 10),
  UserAction(1, 11),
  UserAction(1, 12),
  UserAction(1, 13),
  UserAction(2, 20)
)

val ds = spark.createDataset[UserAction](values)

// Using my user defined aggregation function works:
ds.select(top1.toColumn.name("top1")).show

// +---+----+
// |who|what|
// +---+----+
// |  2|  20|
// +---+----+

// Using a built-in aggregation function over a window works:
ds.select(max("what").over(Window.partitionBy("who"))).show

// +------------------------------------------------------------------------------------------+
// |max(what) OVER (PARTITION BY who ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)|
// +------------------------------------------------------------------------------------------+
// |                                                                                        13|
// |                                                                                        13|
// |                                                                                        13|
// |                                                                                        13|
// |                                                                                        20|
// +------------------------------------------------------------------------------------------+

// Using my user defined aggregation function over a window fails:
ds.select(top1.toColumn.over(Window.partitionBy("who"))).show

// org.apache.spark.sql.AnalysisException: [DATATYPE_MISMATCH.UNSPECIFIED_FRAME] Cannot resolve "(PARTITION BY who unspecifiedframe$())" due to data type mismatch: Cannot use an UnspecifiedFrame. This should have been converted during analysis.;
// 'Aggregate [unresolvedalias($anon$1($anon$1@1b4f42df, None, None, None, knownnotnull(assertnotnull(input[0, UserAction, true])).who, knownnotnull(assertnotnull(input[0, UserAction, true])).what, newInstance(class UserAction), if (isnull(input[0, UserAction, true])) null else named_struct(who, knownnotnull(input[0, UserAction, true]).who, what, knownnotnull(input[0, UserAction, true]).what), StructField(who,LongType,false), StructField(what,LongType,false), true, 0, 0) windowspecdefinition(who#614L, unspecifiedframe$()), Some(org.apache.spark.sql.Column$$Lambda$5083/1086002511@ea147ea))]
// +- LocalRelation [who#614L, what#615L] {code}
 

*Original Description*

The exception from Spark says "Please file a bug report." so here I am.

Seen with Spark 3.1.1.327 on Scala 2.12. Haven't tried other versions.

Error stack trace:
{code:java}
 testTop3ByUser FAILED
    org.apache.spark.sql.AnalysisException: cannot resolve '(PARTITION BY `who` unspecifiedframe$())' due to data type mismatch: Cannot use an UnspecifiedFrame. This should have been converted during analysis. Please file a bug report.;
    'Aggregate [$anon$1(TestSparkTop3$$anon$1@56cd6f12, None, None, None, encodeusingserializer(input[0, java.lang.Object, true], false), decodeusingserializer(input[0, binary, true], scala.collection.mutable.PriorityQueue, false), encodeusingserializer(input[0, java.lang.Object, true], false), BinaryType, true, 0, 0) windowspecdefinition(who#2L, unspecifiedframe$()) AS $anon$1(encodeusingserializer(input[0, java.lang.Object, true], false) AS `value`, decodeusingserializer(input[0, binary, true], scala.collection.mutable.PriorityQueue, false), encodeusingserializer(input[0, java.lang.Object, true], false)) AS `top3` OVER (PARTITION BY who unspecifiedframe$())#15]
    +- LocalRelation [who#2L, what#3L]
        at org.apache.spark.sql.catalyst.analysis.package$AnalysisErrorAt.failAnalysis(package.scala:42)
        at org.apache.spark.sql.catalyst.analysis.CheckAnalysis$$anonfun$$nestedInanonfun$checkAnalysis$1$2.applyOrElse(CheckAnalysis.scala:161)
        at org.apache.spark.sql.catalyst.analysis.CheckAnalysis$$anonfun$$nestedInanonfun$checkAnalysis$1$2.applyOrElse(CheckAnalysis.scala:152)
        at org.apache.spark.sql.catalyst.trees.TreeNode.$anonfun$transformUp$2(TreeNode.scala:347)
        at org.apache.spark.sql.catalyst.trees.CurrentOrigin$.withOrigin(TreeNode.scala:76)
        at org.apache.spark.sql.catalyst.trees.TreeNode.transformUp(TreeNode.scala:347)
        at org.apache.spark.sql.catalyst.trees.TreeNode.$anonfun$transformUp$1(TreeNode.scala:344)
        at org.apache.spark.sql.catalyst.trees.TreeNode.$anonfun$mapChildren$1(TreeNode.scala:413)
        at org.apache.spark.sql.catalyst.trees.TreeNode.mapProductIterator(TreeNode.scala:249)
        at org.apache.spark.sql.catalyst.trees.TreeNode.mapChildren(TreeNode.scala:411)
        at org.apache.spark.sql.catalyst.trees.TreeNode.mapChildren(TreeNode.scala:364)
        at org.apache.spark.sql.catalyst.trees.TreeNode.transformUp(TreeNode.scala:344)
        at org.apache.spark.sql.catalyst.trees.TreeNode.$anonfun$transformUp$1(TreeNode.scala:344)
        at org.apache.spark.sql.catalyst.trees.TreeNode.$anonfun$mapChildren$1(TreeNode.scala:413)
        at org.apache.spark.sql.catalyst.trees.TreeNode.mapProductIterator(TreeNode.scala:249)
        at org.apache.spark.sql.catalyst.trees.TreeNode.mapChildren(TreeNode.scala:411)
        at org.apache.spark.sql.catalyst.trees.TreeNode.mapChildren(TreeNode.scala:364)
        at org.apache.spark.sql.catalyst.trees.TreeNode.transformUp(TreeNode.scala:344)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.$anonfun$transformExpressionsUp$1(QueryPlan.scala:104)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.$anonfun$mapExpressions$1(QueryPlan.scala:116)
        at org.apache.spark.sql.catalyst.trees.CurrentOrigin$.withOrigin(TreeNode.scala:76)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.transformExpression$1(QueryPlan.scala:116)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.recursiveTransform$1(QueryPlan.scala:127)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.$anonfun$mapExpressions$3(QueryPlan.scala:132)
        at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:238)
        at scala.collection.mutable.ResizableArray.foreach(ResizableArray.scala:62)
        at scala.collection.mutable.ResizableArray.foreach$(ResizableArray.scala:55)
        at scala.collection.mutable.ArrayBuffer.foreach(ArrayBuffer.scala:49)
        at scala.collection.TraversableLike.map(TraversableLike.scala:238)
        at scala.collection.TraversableLike.map$(TraversableLike.scala:231)
        at scala.collection.AbstractTraversable.map(Traversable.scala:108)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.recursiveTransform$1(QueryPlan.scala:132)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.$anonfun$mapExpressions$4(QueryPlan.scala:137)
        at org.apache.spark.sql.catalyst.trees.TreeNode.mapProductIterator(TreeNode.scala:249)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.mapExpressions(QueryPlan.scala:137)
        at org.apache.spark.sql.catalyst.plans.QueryPlan.transformExpressionsUp(QueryPlan.scala:104)
        at org.apache.spark.sql.catalyst.analysis.CheckAnalysis.$anonfun$checkAnalysis$1(CheckAnalysis.scala:152)
        at org.apache.spark.sql.catalyst.analysis.CheckAnalysis.$anonfun$checkAnalysis$1$adapted(CheckAnalysis.scala:93)
        at org.apache.spark.sql.catalyst.trees.TreeNode.foreachUp(TreeNode.scala:189)
        at org.apache.spark.sql.catalyst.analysis.CheckAnalysis.checkAnalysis(CheckAnalysis.scala:93)
        at org.apache.spark.sql.catalyst.analysis.CheckAnalysis.checkAnalysis$(CheckAnalysis.scala:90)
        at org.apache.spark.sql.catalyst.analysis.Analyzer.checkAnalysis(Analyzer.scala:155)
        at org.apache.spark.sql.catalyst.analysis.Analyzer.$anonfun$executeAndCheck$1(Analyzer.scala:176)
        at org.apache.spark.sql.catalyst.plans.logical.AnalysisHelper$.markInAnalyzer(AnalysisHelper.scala:228)
        at org.apache.spark.sql.catalyst.analysis.Analyzer.executeAndCheck(Analyzer.scala:173)
        at org.apache.spark.sql.execution.QueryExecution.$anonfun$analyzed$1(QueryExecution.scala:73)
        at org.apache.spark.sql.catalyst.QueryPlanningTracker.measurePhase(QueryPlanningTracker.scala:111)
        at org.apache.spark.sql.execution.QueryExecution.$anonfun$executePhase$1(QueryExecution.scala:143)
        at org.apache.spark.sql.SparkSession.withActive(SparkSession.scala:772)
        at org.apache.spark.sql.execution.QueryExecution.executePhase(QueryExecution.scala:143)
        at org.apache.spark.sql.execution.QueryExecution.analyzed$lzycompute(QueryExecution.scala:73)
        at org.apache.spark.sql.execution.QueryExecution.analyzed(QueryExecution.scala:71)
        at org.apache.spark.sql.execution.QueryExecution.assertAnalyzed(QueryExecution.scala:63)
        at org.apache.spark.sql.Dataset$.$anonfun$ofRows$1(Dataset.scala:90)
        at org.apache.spark.sql.SparkSession.withActive(SparkSession.scala:772)
        at org.apache.spark.sql.Dataset$.ofRows(Dataset.scala:88)
        at org.apache.spark.sql.Dataset.withPlan(Dataset.scala:3715)
        at org.apache.spark.sql.Dataset.select(Dataset.scala:1462)
        at TestSparkTop3.testTop3ByUser(TestSparkTop3.scala:54){code}
Sample unit test to reproduce:
{code:java}
import org.apache.spark.sql.{Encoder, Encoders}
import org.apache.spark.sql.expressions.{Aggregator, Window}
import org.scalatest.Matchers
import org.scalatest.testng.TestNGSuite
import org.testng.annotations.Test

// A record that a user did something.
case class UserAction(who: Long, what: Long)

class TestSparkTop3 extends TestNGSuite with Matchers with TestSparkSession {
  @Test
  def testTop3ByUser(): Unit = {
    import spark.implicits._

    implicit val userActionEncoder: Encoder[UserAction] = Encoders.product[UserAction]

    val top3 = new Aggregator[UserAction, mutable.PriorityQueue[UserAction], List[UserAction]] {
      def limitSize(b: mutable.PriorityQueue[UserAction]): mutable.PriorityQueue[UserAction] = {
        while (b.size > 3) {
          b.dequeue()
        }
        b
      }

      override def zero: mutable.PriorityQueue[UserAction] = mutable.PriorityQueue.empty[UserAction](Ordering.by(_.what))

      override def reduce(b: mutable.PriorityQueue[UserAction], a: UserAction): mutable.PriorityQueue[UserAction] =
        limitSize(b += a)

      override def merge(b1: mutable.PriorityQueue[UserAction],
        b2: mutable.PriorityQueue[UserAction]): mutable.PriorityQueue[UserAction] = limitSize(b1 ++ b2)

      override def finish(reduction: mutable.PriorityQueue[UserAction]): List[UserAction] = reduction.toList

      override def bufferEncoder: Encoder[mutable.PriorityQueue[UserAction]] = Encoders.javaSerialization

      override def outputEncoder: Encoder[List[UserAction]] = Encoders.javaSerialization
    }

    val values = Seq(
      UserAction(1, 10),
      UserAction(1, 11),
      UserAction(1, 12),
      UserAction(1, 13),
      UserAction(2, 20)
    )

    val ds = spark.createDataset[UserAction](values)

    val result = ds.select(top3.toColumn.name("top3").over(Window.partitionBy("who")))
      .collect()
    println(result.map(_.toString).mkString(", "))
  }
} {code}