-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-27905] [SQL] Add higher order function 'forall' #24761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9753f57
abef9cf
1775c03
04822ff
c93e701
af77619
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -445,8 +445,73 @@ case class ArrayExists( | |
| false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Tests whether a predicate holds for all elements in the array. | ||
| */ | ||
| @ExpressionDescription(usage = | ||
| "_FUNC_(expr, pred) - Tests whether a predicate holds for all elements in the array.", | ||
| examples = """ | ||
| Examples: | ||
| > SELECT _FUNC_(array(1, 2, 3), x -> x % 2 == 0); | ||
| false | ||
| > SELECT _FUNC_(array(2, 4, 8), x -> x % 2 == 0); | ||
| true | ||
| > SELECT _FUNC_(array(1, null, 3), x -> x % 2 == 0); | ||
| false | ||
| > SELECT _FUNC_(array(2, null, 8), x -> x % 2 == 0); | ||
| null | ||
| """, | ||
| since = "3.0.0") | ||
| case class ArrayForAll( | ||
| argument: Expression, | ||
| function: Expression) | ||
| extends ArrayBasedSimpleHigherOrderFunction with CodegenFallback { | ||
|
|
||
| override def nullable: Boolean = | ||
| super.nullable || function.nullable | ||
|
|
||
| override def dataType: DataType = BooleanType | ||
|
|
||
| override def functionType: AbstractDataType = BooleanType | ||
|
|
||
| override def prettyName: String = "exists" | ||
| override def bind(f: (Expression, Seq[(DataType, Boolean)]) => LambdaFunction): ArrayForAll = { | ||
| val ArrayType(elementType, containsNull) = argument.dataType | ||
| copy(function = f(function, (elementType, containsNull) :: Nil)) | ||
| } | ||
|
|
||
| @transient lazy val LambdaFunction(_, Seq(elementVar: NamedLambdaVariable), _) = function | ||
|
|
||
| /* | ||
| * true for all non null elements foundNull result | ||
| * F F F | ||
| * F T F | ||
| * T F T | ||
| * T T N | ||
| */ | ||
| override def nullSafeEval(inputRow: InternalRow, argumentValue: Any): Any = { | ||
| val arr = argumentValue.asInstanceOf[ArrayData] | ||
| val f = functionForEval | ||
| var forall = true | ||
| var foundNull = false | ||
| var i = 0 | ||
| while (i < arr.numElements && forall) { | ||
| elementVar.value.set(arr.get(i, elementVar.dataType)) | ||
| val ret = f.eval(inputRow) | ||
| if (ret == null) { | ||
| foundNull = true | ||
| } else if (!ret.asInstanceOf[Boolean]) { | ||
| forall = false | ||
| } | ||
| i += 1 | ||
| } | ||
| if (foundNull && forall) { | ||
| null | ||
| } else { | ||
| forall | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? |
||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2322,6 +2322,116 @@ class DataFrameFunctionsSuite extends QueryTest with SharedSQLContext { | |
| assert(ex4.getMessage.contains("cannot resolve '`a`'")) | ||
| } | ||
|
|
||
| test("forall function - array for primitive type not containing null") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think most/all of this should be covered by unit tests. You can add a single test to validate that the function registry works, if you must. I know you mirrored the tests for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with it, but I remember there was a problem that the behavior was different between the whole stage codegen on/off before, then we decided to do like this as a workaround (#21795). |
||
| val df = Seq( | ||
| Seq(1, 9, 8, 7), | ||
| Seq(2, 4, 6), | ||
| Seq.empty, | ||
| null | ||
| ).toDF("i") | ||
|
|
||
| def testArrayOfPrimitiveTypeNotContainsNull(): Unit = { | ||
| checkAnswer(df.selectExpr("forall(i, x -> x % 2 == 0)"), | ||
| Seq( | ||
| Row(false), | ||
| Row(true), | ||
| Row(true), | ||
| Row(null))) | ||
| } | ||
|
|
||
| // Test with local relation, the Project will be evaluated without codegen | ||
| testArrayOfPrimitiveTypeNotContainsNull() | ||
| // Test with cached relation, the Project will be evaluated with codegen | ||
| df.cache() | ||
| testArrayOfPrimitiveTypeNotContainsNull() | ||
| } | ||
|
|
||
| test("forall function - array for primitive type containing null") { | ||
| val df = Seq[Seq[Integer]]( | ||
| Seq(1, 9, 8, null, 7), | ||
| Seq(2, null, null, 4, 6, null), | ||
| Seq(2, 4, 6, 8), | ||
| Seq.empty, | ||
| null | ||
| ).toDF("i") | ||
|
|
||
| def testArrayOfPrimitiveTypeContainsNull(): Unit = { | ||
| checkAnswer(df.selectExpr("forall(i, x -> x % 2 == 0 or x is null)"), | ||
| Seq( | ||
| Row(false), | ||
| Row(true), | ||
| Row(true), | ||
| Row(true), | ||
| Row(null))) | ||
| checkAnswer(df.selectExpr("forall(i, x -> x % 2 == 0)"), | ||
| Seq( | ||
| Row(false), | ||
| Row(null), | ||
| Row(true), | ||
| Row(true), | ||
| Row(null))) | ||
| } | ||
|
|
||
| // Test with local relation, the Project will be evaluated without codegen | ||
| testArrayOfPrimitiveTypeContainsNull() | ||
| // Test with cached relation, the Project will be evaluated with codegen | ||
| df.cache() | ||
| testArrayOfPrimitiveTypeContainsNull() | ||
| } | ||
|
|
||
| test("forall function - array for non-primitive type") { | ||
| val df = Seq( | ||
| Seq("c", "a", "b"), | ||
| Seq[String](null, null, null, null), | ||
| Seq.empty, | ||
| null | ||
| ).toDF("s") | ||
|
|
||
| def testNonPrimitiveType(): Unit = { | ||
| checkAnswer(df.selectExpr("forall(s, x -> x is null)"), | ||
| Seq( | ||
| Row(false), | ||
| Row(true), | ||
| Row(true), | ||
| Row(null))) | ||
| } | ||
|
|
||
| // Test with local relation, the Project will be evaluated without codegen | ||
| testNonPrimitiveType() | ||
| // Test with cached relation, the Project will be evaluated with codegen | ||
| df.cache() | ||
| testNonPrimitiveType() | ||
| } | ||
|
|
||
| test("forall function - invalid") { | ||
| val df = Seq( | ||
| (Seq("c", "a", "b"), 1), | ||
| (Seq("b", null, "c", null), 2), | ||
| (Seq.empty, 3), | ||
| (null, 4) | ||
| ).toDF("s", "i") | ||
|
|
||
| val ex1 = intercept[AnalysisException] { | ||
| df.selectExpr("forall(s, (x, y) -> x + y)") | ||
| } | ||
| assert(ex1.getMessage.contains("The number of lambda function arguments '2' does not match")) | ||
|
|
||
| val ex2 = intercept[AnalysisException] { | ||
| df.selectExpr("forall(i, x -> x)") | ||
| } | ||
| assert(ex2.getMessage.contains("data type mismatch: argument 1 requires array type")) | ||
|
|
||
| val ex3 = intercept[AnalysisException] { | ||
| df.selectExpr("forall(s, x -> x)") | ||
| } | ||
| assert(ex3.getMessage.contains("data type mismatch: argument 2 requires boolean type")) | ||
|
|
||
| val ex4 = intercept[AnalysisException] { | ||
| df.selectExpr("forall(a, x -> x)") | ||
| } | ||
| assert(ex4.getMessage.contains("cannot resolve '`a`'")) | ||
| } | ||
|
|
||
| test("aggregate function - array for primitive type not containing null") { | ||
| val df = Seq( | ||
| Seq(1, 9, 8, 7), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to factor out the bind definition as well here, but there seems to be a known issue on trying to rely on the generated copy method of a case class in its parent trait: https://www.scala-lang.org/old/node/6369
Aside: this is also the same bind method for ArrayFilter