joelrobin18 commented on code in PR #57958:
URL: https://github.com/apache/spark/pull/57958#discussion_r3766802276


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/DatasketchesHllSketchSuite.scala:
##########
@@ -153,4 +153,130 @@ class DatasketchesHllSketchSuite extends SparkFunSuite {
         s"but got: ${exception.getClass.getName}: ${exception.getMessage}"
     )
   }
+  /** Runs HllUnionAgg over `inputs` (a NULL entry stands for a NULL sketch) 
for a single group. */
+  private def unionAgg(inputs: Seq[Any], allowDifferentLgConfigK: Boolean): 
Array[Byte] = {
+    val aggFunc = new HllUnionAgg(
+      BoundReference(0, BinaryType, nullable = true), allowDifferentLgConfigK)
+    val buffer = inputs.foldLeft(aggFunc.createAggregationBuffer()) { (buf, 
input) =>
+      aggFunc.update(buf, InternalRow(input))
+    }
+    aggFunc.eval(buffer).asInstanceOf[Array[Byte]]
+  }
+
+  /** Runs HllSketchAgg at `lgConfigK` over `values` (a NULL entry stands for 
a NULL value). */
+  private def sketchAgg(values: Seq[Any], lgConfigK: Int): Array[Byte] = {
+    val aggFunc = new HllSketchAgg(BoundReference(0, StringType, nullable = 
true), lgConfigK)
+    val buffer = values.foldLeft(aggFunc.createAggregationBuffer()) { (buf, 
value) =>
+      aggFunc.update(buf, InternalRow(value))
+    }
+    aggFunc.eval(buffer).asInstanceOf[Array[Byte]]
+  }
+
+  /** Evaluates the scalar hll_union over two serialized sketches. */
+  private def scalarUnion(
+      left: Array[Byte], right: Array[Byte], allowDifferentLgConfigK: 
Boolean): Array[Byte] =
+    HllUnion(
+      Literal(left, BinaryType),
+      Literal(right, BinaryType),
+      
Literal(allowDifferentLgConfigK)).eval(InternalRow.empty).asInstanceOf[Array[Byte]]
+
+  private def lgConfigKOf(sketch: Array[Byte]): Int =
+    HllSketch.heapify(Memory.wrap(sketch)).getLgConfigK
+
+  private def estimateOf(sketch: Array[Byte]): Long =
+    HllSketchEstimate(BoundReference(0, BinaryType, nullable = true))
+      .eval(InternalRow(sketch)).asInstanceOf[Long]
+
+  private def stringValues(n: Int): Seq[Any] =
+    Seq.tabulate(n)(i => UTF8String.fromString(i.toString))
+
+  test("hll_union_agg on a group with no non-NULL sketch yields an empty 
default-lgConfigK " +
+    "sketch") {
+    // The aggregate has no lgConfigK parameter and never saw a sketch, so it 
has no precision to
+    // report and falls back to the Datasketches default. Documented here 
because the resulting
+    // sketch is observable, and must stay harmless to later unions (see the 
tests below).
+    val allNull = unionAgg(Seq(null, null), allowDifferentLgConfigK = false)
+    assert(estimateOf(allNull) == 0L)
+    assert(lgConfigKOf(allNull) == HllSketch.DEFAULT_LG_K)
+
+    // hll_sketch_agg does not share the problem only because it has the 
parameter: it builds its
+    // buffer eagerly at the requested lgConfigK. Without the argument it 
defaults to 12 as well.
+    val emptyAt15 = sketchAgg(Seq(null, null), 15)
+    assert(estimateOf(emptyAt15) == 0L)
+    assert(lgConfigKOf(emptyAt15) == 15)
+  }
+
+  test("hll_union_agg merges an empty sketch of a different lgConfigK without 
an error") {
+    // An empty sketch holds no coupons, so unioning it cannot lose 
information at any lgConfigK.
+    // This is the shape produced by the test above, i.e. what a persisted 
table ends up holding
+    // for a group whose sketches were all NULL.
+    val emptyAtDefaultLgK = unionAgg(Seq(null), allowDifferentLgConfigK = 
false)
+    val sketchAt15 = sketchAgg(stringValues(1000), 15)
+
+    Seq(true, false).foreach { allowDifferentLgConfigK =>
+      Seq(
+        ("empty sketch first", Seq[Any](emptyAtDefaultLgK, sketchAt15)),
+        ("empty sketch last", Seq[Any](sketchAt15, emptyAtDefaultLgK))
+      ).foreach { case (order, inputs) =>
+        val merged = unionAgg(inputs, allowDifferentLgConfigK)
+        // The non-empty sketch decides the precision, whichever order the 
rows arrive in: the
+        // result must not depend on which row the aggregate happens to see 
first.
+        assert(lgConfigKOf(merged) == 15,
+          s"$order (allowDifferentLgConfigK=$allowDifferentLgConfigK) changed 
the lgConfigK")
+        assert(estimateOf(merged) == estimateOf(sketchAt15),
+          s"$order (allowDifferentLgConfigK=$allowDifferentLgConfigK) changed 
the estimate")
+      }
+    }
+  }
+
+  test("hll_union_agg still rejects non-empty sketches with different 
lgConfigK") {
+    val sketchAt12 = sketchAgg(stringValues(1000), 12)
+    val sketchAt15 = sketchAgg(stringValues(1000), 15)
+
+    Seq(
+      Seq[Any](sketchAt12, sketchAt15),
+      Seq[Any](sketchAt15, sketchAt12)
+    ).foreach { inputs =>
+      val exception = intercept[SparkRuntimeException] {
+        unionAgg(inputs, allowDifferentLgConfigK = false)
+      }
+      assert(exception.getCondition == "HLL_UNION_DIFFERENT_LG_K")
+    }
+
+    // And still downsamples rather than erroring when the caller opts in.
+    assert(lgConfigKOf(unionAgg(Seq(sketchAt15, sketchAt12), 
allowDifferentLgConfigK = true)) <= 15)

Review Comment:
   Updated



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