maropu commented on a change in pull request #28194: URL: https://github.com/apache/spark/pull/28194#discussion_r411131393
########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo Review comment: Plz remove this. ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString Review comment: ditto. ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. Review comment: nit: `SQL schemas` -> `SQL output schemas` ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. + * The golden result file is "spark/sql/core/src/test/resources/sql-functions/sql-expression-schema.md". + * + * To run the entire test suite: + * {{{ + * build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * To re-generate golden files for entire suite, run: + * {{{ + * SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * For example: + * {{{ + * ... + * @ExpressionDescription( + * usage = "_FUNC_(str, n) - Returns the string which repeats the given string value n times.", + * examples = """ + * Examples: + * > SELECT _FUNC_('123', 2); + * 123123 + * """, + * since = "1.5.0") + * case class StringRepeat(str: Expression, times: Expression) + * ... + * }}} + * + * The format for golden result files look roughly like: + * {{{ + * ... + * | 238 | org.apache.spark.sql.catalyst.expressions.StringRepeat | repeat | SELECT repeat('123', 2) | struct<repeat(123, 2):string> | + * ... + * }}} + */ +// scalastyle:on line.size.limit +@ExtendedSQLTest +class ExpressionsSchemaSuite extends QueryTest with SharedSparkSession { + + private val regenerateGoldenFiles: Boolean = System.getenv("SPARK_GENERATE_GOLDEN_FILES") == "1" + + private val baseResourcePath = { + // We use a path based on Spark home for 2 reasons: + // 1. Maven can't get correct resource directory when resources in other jars. + // 2. We test subclasses in the hive-thriftserver module. + val sparkHome = { + assert(sys.props.contains("spark.test.home") || + sys.env.contains("SPARK_HOME"), "spark.test.home or SPARK_HOME is not set.") + sys.props.getOrElse("spark.test.home", sys.env("SPARK_HOME")) + } + + java.nio.file.Paths.get(sparkHome, + "sql", "core", "src", "test", "resources", "sql-functions").toFile + } + + private val resultFile = new File(baseResourcePath, "sql-expression-schema.md") + + val ignoreSet = Set( + // One of examples shows getting the current timestamp + "org.apache.spark.sql.catalyst.expressions.UnixTimestamp", + // Random output without a seed + "org.apache.spark.sql.catalyst.expressions.Rand", + "org.apache.spark.sql.catalyst.expressions.Randn", + "org.apache.spark.sql.catalyst.expressions.Shuffle", + "org.apache.spark.sql.catalyst.expressions.Uuid", + // The example calls methods that return unstable results. + "org.apache.spark.sql.catalyst.expressions.CallMethodViaReflection") + + val MISSING_EXAMPLE = "Example is missing" + + /** A single SQL query's SQL and schema. */ + protected case class QueryOutput( + number: String = "0", + className: String, + funcName: String, + sql: String = MISSING_EXAMPLE, + schema: String = MISSING_EXAMPLE) { Review comment: nit: I think we don't need the variable, how about just writing them like this? ``` funcName: String, sql: String = "N/A", schema: String = "N/A") { ``` ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. + * The golden result file is "spark/sql/core/src/test/resources/sql-functions/sql-expression-schema.md". + * + * To run the entire test suite: + * {{{ + * build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * To re-generate golden files for entire suite, run: + * {{{ + * SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * For example: + * {{{ + * ... + * @ExpressionDescription( + * usage = "_FUNC_(str, n) - Returns the string which repeats the given string value n times.", + * examples = """ + * Examples: + * > SELECT _FUNC_('123', 2); + * 123123 + * """, + * since = "1.5.0") + * case class StringRepeat(str: Expression, times: Expression) + * ... + * }}} + * + * The format for golden result files look roughly like: + * {{{ + * ... + * | 238 | org.apache.spark.sql.catalyst.expressions.StringRepeat | repeat | SELECT repeat('123', 2) | struct<repeat(123, 2):string> | + * ... + * }}} + */ +// scalastyle:on line.size.limit +@ExtendedSQLTest +class ExpressionsSchemaSuite extends QueryTest with SharedSparkSession { + + private val regenerateGoldenFiles: Boolean = System.getenv("SPARK_GENERATE_GOLDEN_FILES") == "1" + + private val baseResourcePath = { + // We use a path based on Spark home for 2 reasons: + // 1. Maven can't get correct resource directory when resources in other jars. + // 2. We test subclasses in the hive-thriftserver module. + val sparkHome = { + assert(sys.props.contains("spark.test.home") || + sys.env.contains("SPARK_HOME"), "spark.test.home or SPARK_HOME is not set.") + sys.props.getOrElse("spark.test.home", sys.env("SPARK_HOME")) + } + + java.nio.file.Paths.get(sparkHome, + "sql", "core", "src", "test", "resources", "sql-functions").toFile + } + + private val resultFile = new File(baseResourcePath, "sql-expression-schema.md") + + val ignoreSet = Set( + // One of examples shows getting the current timestamp + "org.apache.spark.sql.catalyst.expressions.UnixTimestamp", + // Random output without a seed + "org.apache.spark.sql.catalyst.expressions.Rand", + "org.apache.spark.sql.catalyst.expressions.Randn", + "org.apache.spark.sql.catalyst.expressions.Shuffle", + "org.apache.spark.sql.catalyst.expressions.Uuid", + // The example calls methods that return unstable results. + "org.apache.spark.sql.catalyst.expressions.CallMethodViaReflection") + + val MISSING_EXAMPLE = "Example is missing" + + /** A single SQL query's SQL and schema. */ + protected case class QueryOutput( + number: String = "0", + className: String, + funcName: String, + sql: String = MISSING_EXAMPLE, + schema: String = MISSING_EXAMPLE) { + override def toString: String = { + s"| $number | $className | $funcName | $sql | $schema |" + } + } + + test("Check schemas for expression examples") { + val exampleRe = """^(.+);\n(?s)(.+)$""".r + val funInfos = spark.sessionState.functionRegistry.listFunction().map { funcId => + spark.sessionState.catalog.lookupFunctionInfo(funcId) + } + + val classFunsMap = funInfos.groupBy(_.getClassName).toSeq.sortBy(_._1) + val outputBuffer = new ArrayBuffer[String] + val outputs = new ArrayBuffer[QueryOutput] + val missingExamples = new ArrayBuffer[String] + + var _curNumber = 0 + def curNumber: String = { + _curNumber += 1 + _curNumber.toString + } + + classFunsMap.foreach { kv => + val className = kv._1 + if (!ignoreSet.contains(className)) { + kv._2.foreach { funInfo => + val example = funInfo.getExamples + if (example == "") { + val queryOutput = QueryOutput(curNumber, className, funInfo.getName) + outputBuffer += queryOutput.toString + outputs += queryOutput + missingExamples += queryOutput.funcName + } + + // If expression exists 'Examples' segment, the first element is 'Examples'. Because + // this test case is only used to print aliases of expressions for double checking. + // Therefore, we only need to output the first SQL and its corresponding schema. + // Note: We need to filter out the commands that set the parameters, such as: + // SET spark.sql.parser.escapedStringLiterals=true + example.split(" > ").tail + .filterNot(_.trim.startsWith("SET")).take(1).foreach(_ match { + case exampleRe(sql, expected) => + val df = spark.sql(sql) + val schema = df.schema.catalogString + val queryOutput = QueryOutput(curNumber, className, funInfo.getName, sql, schema) + outputBuffer += queryOutput.toString + outputs += queryOutput + case _ => + }) + } + } + } + + if (regenerateGoldenFiles) { + val missingExampleStr = missingExamples.mkString(",") + val goldenOutput = { + "## Summary\n" + + s" - Number of queries: ${outputs.size}\n" + + s" - Number of expressions that missing example: ${missingExamples.size}\n" + + s" - Expressions missing examples: $missingExampleStr\n" + + "## Schema of Built-in Functions\n" + + "| No | Class name | Function name or alias | Query example | Output schema |\n" + + "| -- | ---------- | ---------------------- | ------------- | ------------- |\n" + Review comment: Could we use a format like this? ``` s""" |## Summary | - Number of queries: ${outputs.size} | - ... """.stripMargin ``` ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. + * The golden result file is "spark/sql/core/src/test/resources/sql-functions/sql-expression-schema.md". + * + * To run the entire test suite: + * {{{ + * build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * To re-generate golden files for entire suite, run: + * {{{ + * SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * For example: + * {{{ + * ... + * @ExpressionDescription( + * usage = "_FUNC_(str, n) - Returns the string which repeats the given string value n times.", + * examples = """ + * Examples: + * > SELECT _FUNC_('123', 2); + * 123123 + * """, + * since = "1.5.0") + * case class StringRepeat(str: Expression, times: Expression) + * ... + * }}} + * + * The format for golden result files look roughly like: + * {{{ + * ... + * | 238 | org.apache.spark.sql.catalyst.expressions.StringRepeat | repeat | SELECT repeat('123', 2) | struct<repeat(123, 2):string> | + * ... + * }}} + */ +// scalastyle:on line.size.limit +@ExtendedSQLTest +class ExpressionsSchemaSuite extends QueryTest with SharedSparkSession { + + private val regenerateGoldenFiles: Boolean = System.getenv("SPARK_GENERATE_GOLDEN_FILES") == "1" + + private val baseResourcePath = { + // We use a path based on Spark home for 2 reasons: + // 1. Maven can't get correct resource directory when resources in other jars. + // 2. We test subclasses in the hive-thriftserver module. + val sparkHome = { + assert(sys.props.contains("spark.test.home") || + sys.env.contains("SPARK_HOME"), "spark.test.home or SPARK_HOME is not set.") + sys.props.getOrElse("spark.test.home", sys.env("SPARK_HOME")) + } + + java.nio.file.Paths.get(sparkHome, + "sql", "core", "src", "test", "resources", "sql-functions").toFile + } + + private val resultFile = new File(baseResourcePath, "sql-expression-schema.md") + + val ignoreSet = Set( + // One of examples shows getting the current timestamp + "org.apache.spark.sql.catalyst.expressions.UnixTimestamp", + // Random output without a seed + "org.apache.spark.sql.catalyst.expressions.Rand", + "org.apache.spark.sql.catalyst.expressions.Randn", + "org.apache.spark.sql.catalyst.expressions.Shuffle", + "org.apache.spark.sql.catalyst.expressions.Uuid", + // The example calls methods that return unstable results. + "org.apache.spark.sql.catalyst.expressions.CallMethodViaReflection") Review comment: We need to ignore these cases above in this test? It looks like the results are non-deterministic, but the output types are deterministic? ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. + * The golden result file is "spark/sql/core/src/test/resources/sql-functions/sql-expression-schema.md". + * + * To run the entire test suite: + * {{{ + * build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * To re-generate golden files for entire suite, run: + * {{{ + * SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * For example: + * {{{ + * ... + * @ExpressionDescription( + * usage = "_FUNC_(str, n) - Returns the string which repeats the given string value n times.", + * examples = """ + * Examples: + * > SELECT _FUNC_('123', 2); + * 123123 + * """, + * since = "1.5.0") + * case class StringRepeat(str: Expression, times: Expression) + * ... + * }}} + * + * The format for golden result files look roughly like: + * {{{ + * ... + * | 238 | org.apache.spark.sql.catalyst.expressions.StringRepeat | repeat | SELECT repeat('123', 2) | struct<repeat(123, 2):string> | + * ... + * }}} + */ +// scalastyle:on line.size.limit +@ExtendedSQLTest +class ExpressionsSchemaSuite extends QueryTest with SharedSparkSession { + + private val regenerateGoldenFiles: Boolean = System.getenv("SPARK_GENERATE_GOLDEN_FILES") == "1" + + private val baseResourcePath = { + // We use a path based on Spark home for 2 reasons: + // 1. Maven can't get correct resource directory when resources in other jars. + // 2. We test subclasses in the hive-thriftserver module. + val sparkHome = { + assert(sys.props.contains("spark.test.home") || + sys.env.contains("SPARK_HOME"), "spark.test.home or SPARK_HOME is not set.") + sys.props.getOrElse("spark.test.home", sys.env("SPARK_HOME")) + } + + java.nio.file.Paths.get(sparkHome, + "sql", "core", "src", "test", "resources", "sql-functions").toFile + } + + private val resultFile = new File(baseResourcePath, "sql-expression-schema.md") + + val ignoreSet = Set( + // One of examples shows getting the current timestamp + "org.apache.spark.sql.catalyst.expressions.UnixTimestamp", + // Random output without a seed + "org.apache.spark.sql.catalyst.expressions.Rand", + "org.apache.spark.sql.catalyst.expressions.Randn", + "org.apache.spark.sql.catalyst.expressions.Shuffle", + "org.apache.spark.sql.catalyst.expressions.Uuid", + // The example calls methods that return unstable results. + "org.apache.spark.sql.catalyst.expressions.CallMethodViaReflection") + + val MISSING_EXAMPLE = "Example is missing" + + /** A single SQL query's SQL and schema. */ + protected case class QueryOutput( + number: String = "0", + className: String, + funcName: String, + sql: String = MISSING_EXAMPLE, + schema: String = MISSING_EXAMPLE) { + override def toString: String = { + s"| $number | $className | $funcName | $sql | $schema |" + } + } + + test("Check schemas for expression examples") { + val exampleRe = """^(.+);\n(?s)(.+)$""".r + val funInfos = spark.sessionState.functionRegistry.listFunction().map { funcId => + spark.sessionState.catalog.lookupFunctionInfo(funcId) + } + + val classFunsMap = funInfos.groupBy(_.getClassName).toSeq.sortBy(_._1) + val outputBuffer = new ArrayBuffer[String] + val outputs = new ArrayBuffer[QueryOutput] + val missingExamples = new ArrayBuffer[String] + + var _curNumber = 0 + def curNumber: String = { + _curNumber += 1 + _curNumber.toString + } + + classFunsMap.foreach { kv => + val className = kv._1 + if (!ignoreSet.contains(className)) { + kv._2.foreach { funInfo => + val example = funInfo.getExamples + if (example == "") { + val queryOutput = QueryOutput(curNumber, className, funInfo.getName) + outputBuffer += queryOutput.toString + outputs += queryOutput + missingExamples += queryOutput.funcName + } + + // If expression exists 'Examples' segment, the first element is 'Examples'. Because + // this test case is only used to print aliases of expressions for double checking. + // Therefore, we only need to output the first SQL and its corresponding schema. + // Note: We need to filter out the commands that set the parameters, such as: + // SET spark.sql.parser.escapedStringLiterals=true + example.split(" > ").tail + .filterNot(_.trim.startsWith("SET")).take(1).foreach(_ match { + case exampleRe(sql, expected) => Review comment: nit: `case exampleRe(sql, _) =>` ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. + * The golden result file is "spark/sql/core/src/test/resources/sql-functions/sql-expression-schema.md". + * + * To run the entire test suite: + * {{{ + * build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * To re-generate golden files for entire suite, run: + * {{{ + * SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * For example: + * {{{ + * ... + * @ExpressionDescription( + * usage = "_FUNC_(str, n) - Returns the string which repeats the given string value n times.", + * examples = """ + * Examples: + * > SELECT _FUNC_('123', 2); + * 123123 + * """, + * since = "1.5.0") + * case class StringRepeat(str: Expression, times: Expression) + * ... + * }}} + * + * The format for golden result files look roughly like: + * {{{ + * ... + * | 238 | org.apache.spark.sql.catalyst.expressions.StringRepeat | repeat | SELECT repeat('123', 2) | struct<repeat(123, 2):string> | + * ... + * }}} + */ +// scalastyle:on line.size.limit +@ExtendedSQLTest +class ExpressionsSchemaSuite extends QueryTest with SharedSparkSession { + + private val regenerateGoldenFiles: Boolean = System.getenv("SPARK_GENERATE_GOLDEN_FILES") == "1" + + private val baseResourcePath = { + // We use a path based on Spark home for 2 reasons: + // 1. Maven can't get correct resource directory when resources in other jars. + // 2. We test subclasses in the hive-thriftserver module. + val sparkHome = { + assert(sys.props.contains("spark.test.home") || + sys.env.contains("SPARK_HOME"), "spark.test.home or SPARK_HOME is not set.") + sys.props.getOrElse("spark.test.home", sys.env("SPARK_HOME")) + } + + java.nio.file.Paths.get(sparkHome, + "sql", "core", "src", "test", "resources", "sql-functions").toFile + } + + private val resultFile = new File(baseResourcePath, "sql-expression-schema.md") + + val ignoreSet = Set( + // One of examples shows getting the current timestamp + "org.apache.spark.sql.catalyst.expressions.UnixTimestamp", + // Random output without a seed + "org.apache.spark.sql.catalyst.expressions.Rand", + "org.apache.spark.sql.catalyst.expressions.Randn", + "org.apache.spark.sql.catalyst.expressions.Shuffle", + "org.apache.spark.sql.catalyst.expressions.Uuid", + // The example calls methods that return unstable results. + "org.apache.spark.sql.catalyst.expressions.CallMethodViaReflection") + + val MISSING_EXAMPLE = "Example is missing" + + /** A single SQL query's SQL and schema. */ + protected case class QueryOutput( + number: String = "0", + className: String, + funcName: String, + sql: String = MISSING_EXAMPLE, + schema: String = MISSING_EXAMPLE) { + override def toString: String = { + s"| $number | $className | $funcName | $sql | $schema |" + } + } + + test("Check schemas for expression examples") { + val exampleRe = """^(.+);\n(?s)(.+)$""".r + val funInfos = spark.sessionState.functionRegistry.listFunction().map { funcId => + spark.sessionState.catalog.lookupFunctionInfo(funcId) + } + + val classFunsMap = funInfos.groupBy(_.getClassName).toSeq.sortBy(_._1) + val outputBuffer = new ArrayBuffer[String] + val outputs = new ArrayBuffer[QueryOutput] + val missingExamples = new ArrayBuffer[String] + + var _curNumber = 0 + def curNumber: String = { + _curNumber += 1 + _curNumber.toString + } + + classFunsMap.foreach { kv => + val className = kv._1 + if (!ignoreSet.contains(className)) { + kv._2.foreach { funInfo => + val example = funInfo.getExamples + if (example == "") { + val queryOutput = QueryOutput(curNumber, className, funInfo.getName) + outputBuffer += queryOutput.toString + outputs += queryOutput + missingExamples += queryOutput.funcName + } + + // If expression exists 'Examples' segment, the first element is 'Examples'. Because + // this test case is only used to print aliases of expressions for double checking. + // Therefore, we only need to output the first SQL and its corresponding schema. + // Note: We need to filter out the commands that set the parameters, such as: + // SET spark.sql.parser.escapedStringLiterals=true + example.split(" > ").tail + .filterNot(_.trim.startsWith("SET")).take(1).foreach(_ match { Review comment: nit: you don't need the line break and `match`; ``` example.split(" > ").tail.filterNot(_.trim.startsWith("SET")).take(1).foreach { ``` ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. + * The golden result file is "spark/sql/core/src/test/resources/sql-functions/sql-expression-schema.md". + * + * To run the entire test suite: + * {{{ + * build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * To re-generate golden files for entire suite, run: + * {{{ + * SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * For example: + * {{{ + * ... + * @ExpressionDescription( + * usage = "_FUNC_(str, n) - Returns the string which repeats the given string value n times.", + * examples = """ + * Examples: + * > SELECT _FUNC_('123', 2); + * 123123 + * """, + * since = "1.5.0") + * case class StringRepeat(str: Expression, times: Expression) + * ... + * }}} + * + * The format for golden result files look roughly like: + * {{{ + * ... + * | 238 | org.apache.spark.sql.catalyst.expressions.StringRepeat | repeat | SELECT repeat('123', 2) | struct<repeat(123, 2):string> | + * ... + * }}} + */ +// scalastyle:on line.size.limit +@ExtendedSQLTest +class ExpressionsSchemaSuite extends QueryTest with SharedSparkSession { + + private val regenerateGoldenFiles: Boolean = System.getenv("SPARK_GENERATE_GOLDEN_FILES") == "1" + + private val baseResourcePath = { + // We use a path based on Spark home for 2 reasons: + // 1. Maven can't get correct resource directory when resources in other jars. + // 2. We test subclasses in the hive-thriftserver module. + val sparkHome = { + assert(sys.props.contains("spark.test.home") || + sys.env.contains("SPARK_HOME"), "spark.test.home or SPARK_HOME is not set.") + sys.props.getOrElse("spark.test.home", sys.env("SPARK_HOME")) + } + + java.nio.file.Paths.get(sparkHome, + "sql", "core", "src", "test", "resources", "sql-functions").toFile + } + + private val resultFile = new File(baseResourcePath, "sql-expression-schema.md") + + val ignoreSet = Set( + // One of examples shows getting the current timestamp + "org.apache.spark.sql.catalyst.expressions.UnixTimestamp", + // Random output without a seed + "org.apache.spark.sql.catalyst.expressions.Rand", + "org.apache.spark.sql.catalyst.expressions.Randn", + "org.apache.spark.sql.catalyst.expressions.Shuffle", + "org.apache.spark.sql.catalyst.expressions.Uuid", + // The example calls methods that return unstable results. + "org.apache.spark.sql.catalyst.expressions.CallMethodViaReflection") + + val MISSING_EXAMPLE = "Example is missing" + + /** A single SQL query's SQL and schema. */ + protected case class QueryOutput( + number: String = "0", + className: String, + funcName: String, + sql: String = MISSING_EXAMPLE, + schema: String = MISSING_EXAMPLE) { + override def toString: String = { + s"| $number | $className | $funcName | $sql | $schema |" + } + } + + test("Check schemas for expression examples") { + val exampleRe = """^(.+);\n(?s)(.+)$""".r + val funInfos = spark.sessionState.functionRegistry.listFunction().map { funcId => + spark.sessionState.catalog.lookupFunctionInfo(funcId) + } + + val classFunsMap = funInfos.groupBy(_.getClassName).toSeq.sortBy(_._1) Review comment: The current implementation sorts them alphabetically by class name. But, when adding a new expression and re-generating the golden file, I think the diff. of the file tends to be larger. To mitigate this issue, could we sort them by a different item, e.g., `since`? Or, how about removing the column `No`? ########## File path: sql/core/src/test/scala/org/apache/spark/sql/ExpressionsSchemaSuite.scala ########## @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql + +import java.io.File + +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.sql.catalyst.expressions.ExpressionInfo +import org.apache.spark.sql.catalyst.util.{fileToString, stringToFile} +import org.apache.spark.sql.execution.HiveResult.hiveResultString +import org.apache.spark.sql.test.SharedSparkSession +import org.apache.spark.tags.ExtendedSQLTest + +// scalastyle:off line.size.limit +/** + * End-to-end test cases for SQL schemas of expression examples. + * The golden result file is "spark/sql/core/src/test/resources/sql-functions/sql-expression-schema.md". + * + * To run the entire test suite: + * {{{ + * build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * To re-generate golden files for entire suite, run: + * {{{ + * SPARK_GENERATE_GOLDEN_FILES=1 build/sbt "sql/test-only *ExpressionsSchemaSuite" + * }}} + * + * For example: + * {{{ + * ... + * @ExpressionDescription( + * usage = "_FUNC_(str, n) - Returns the string which repeats the given string value n times.", + * examples = """ + * Examples: + * > SELECT _FUNC_('123', 2); + * 123123 + * """, + * since = "1.5.0") + * case class StringRepeat(str: Expression, times: Expression) + * ... + * }}} + * + * The format for golden result files look roughly like: + * {{{ + * ... + * | 238 | org.apache.spark.sql.catalyst.expressions.StringRepeat | repeat | SELECT repeat('123', 2) | struct<repeat(123, 2):string> | + * ... + * }}} + */ +// scalastyle:on line.size.limit +@ExtendedSQLTest +class ExpressionsSchemaSuite extends QueryTest with SharedSparkSession { + + private val regenerateGoldenFiles: Boolean = System.getenv("SPARK_GENERATE_GOLDEN_FILES") == "1" + + private val baseResourcePath = { + // We use a path based on Spark home for 2 reasons: + // 1. Maven can't get correct resource directory when resources in other jars. + // 2. We test subclasses in the hive-thriftserver module. + val sparkHome = { + assert(sys.props.contains("spark.test.home") || + sys.env.contains("SPARK_HOME"), "spark.test.home or SPARK_HOME is not set.") + sys.props.getOrElse("spark.test.home", sys.env("SPARK_HOME")) + } + + java.nio.file.Paths.get(sparkHome, + "sql", "core", "src", "test", "resources", "sql-functions").toFile + } + + private val resultFile = new File(baseResourcePath, "sql-expression-schema.md") + + val ignoreSet = Set( + // One of examples shows getting the current timestamp + "org.apache.spark.sql.catalyst.expressions.UnixTimestamp", + // Random output without a seed + "org.apache.spark.sql.catalyst.expressions.Rand", + "org.apache.spark.sql.catalyst.expressions.Randn", + "org.apache.spark.sql.catalyst.expressions.Shuffle", + "org.apache.spark.sql.catalyst.expressions.Uuid", + // The example calls methods that return unstable results. + "org.apache.spark.sql.catalyst.expressions.CallMethodViaReflection") + + val MISSING_EXAMPLE = "Example is missing" + + /** A single SQL query's SQL and schema. */ + protected case class QueryOutput( + number: String = "0", + className: String, + funcName: String, + sql: String = MISSING_EXAMPLE, + schema: String = MISSING_EXAMPLE) { + override def toString: String = { + s"| $number | $className | $funcName | $sql | $schema |" + } + } + + test("Check schemas for expression examples") { + val exampleRe = """^(.+);\n(?s)(.+)$""".r + val funInfos = spark.sessionState.functionRegistry.listFunction().map { funcId => + spark.sessionState.catalog.lookupFunctionInfo(funcId) + } + + val classFunsMap = funInfos.groupBy(_.getClassName).toSeq.sortBy(_._1) + val outputBuffer = new ArrayBuffer[String] + val outputs = new ArrayBuffer[QueryOutput] + val missingExamples = new ArrayBuffer[String] + + var _curNumber = 0 Review comment: nit: we don't need `_` in the head. ---------------------------------------------------------------- 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. For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
