This is an automated email from the ASF dual-hosted git repository. maxgekk pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push: new 2ba0eab9db25 [SPARK-51562][SQL] Add `time` function 2ba0eab9db25 is described below commit 2ba0eab9db25e1e5958f9bb5bcc58b97c8c6c505 Author: Uros Bojanic <uros.boja...@databricks.com> AuthorDate: Fri Jul 18 21:01:28 2025 +0200 [SPARK-51562][SQL] Add `time` function ### What changes were proposed in this pull request? Add new function `time`, which should cast an `expr` to TIME type. Syntax ``` time(expr) ``` Arguments - `expr`: Any expression that can be cast to TIMESTAMP. Returns - A TIME. Note that this function is a synonym for CAST(expr AS TIME). ### Why are the changes needed? Expanding function coverage for TIME type. ### Does this PR introduce _any_ user-facing change? Yes, this PR adds the new `time` function. ### How was this patch tested? Added new expression-level unit tests, and e2e SQL tests in both ANSI and non-ANSI modes. ### Was this patch authored or co-authored using generative AI tooling? No. Closes #51551 from uros-db/time_func. Authored-by: Uros Bojanic <uros.boja...@databricks.com> Signed-off-by: Max Gekk <max.g...@gmail.com> --- .../sql/catalyst/analysis/FunctionRegistry.scala | 1 + .../sql/catalyst/expressions/CastSuiteBase.scala | 27 ++++++++ .../sql-functions/sql-expression-schema.md | 3 +- .../sql-tests/analyzer-results/time.sql.out | 70 +++++++++++++++++++ .../src/test/resources/sql-tests/inputs/time.sql | 14 ++++ .../test/resources/sql-tests/results/time.sql.out | 80 ++++++++++++++++++++++ 6 files changed, 194 insertions(+), 1 deletion(-) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala index 76c3b1d80b29..d84379c223de 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala @@ -873,6 +873,7 @@ object FunctionRegistry { castAlias("decimal", DecimalType.USER_DEFAULT), castAlias("date", DateType), castAlias("timestamp", TimestampType), + castAlias("time", TimeType()), castAlias("binary", BinaryType), castAlias("string", StringType), diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala index a02b4276628c..44638cede3ec 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala @@ -1588,6 +1588,33 @@ abstract class CastSuiteBase extends SparkFunSuite with ExpressionEvalHelper { } } + test("SPARK-51562: cast alias - time function") { + import org.apache.spark.sql.catalyst.analysis.FunctionRegistry + import org.apache.spark.sql.catalyst.FunctionIdentifier + // Test that time() function is registered and works correctly. + val registry = FunctionRegistry.builtin + val timeFunction = registry.lookupFunctionBuilder(FunctionIdentifier("time")) + assert(timeFunction.isDefined, "time function should be registered in FunctionRegistry") + // Test that time() function creates a proper Cast expression. + val stringInput = Literal("12:34:56") + val timeExpr = timeFunction.get(Seq(stringInput)) + assert(timeExpr.isInstanceOf[Cast]) + // The return type of the cast expression should be TimeType(). + val castExpr = timeExpr.asInstanceOf[Cast] + assert(castExpr.dataType === TimeType()) + + // Test basic string to time conversions using the alias. + checkEvaluation(timeExpr, localTime(12, 34, 56)) + val timeExprWithMillis = timeFunction.get(Seq(Literal("12:34:56.789"))) + checkEvaluation(timeExprWithMillis, localTime(12, 34, 56, 789000)) + val timeExprWithMicros = timeFunction.get(Seq(Literal("12:34:56.789012"))) + checkEvaluation(timeExprWithMicros, localTime(12, 34, 56, 789012)) + + // Test null inputs. + val timeExprNull = timeFunction.get(Seq(Literal.create(null, StringType))) + checkEvaluation(timeExprNull, null) + } + test("cast time to time") { checkEvaluation(cast(Literal(localTime(), TimeType(0)), TimeType(0)), 0L) checkEvaluation(cast(Literal(localTime(0, 0, 0, 1), TimeType(6)), TimeType(6)), diff --git a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md index 3b0b21b9cd77..7aefb5d58f59 100644 --- a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md +++ b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md @@ -74,6 +74,7 @@ | org.apache.spark.sql.catalyst.expressions.Cast | int | N/A | N/A | | org.apache.spark.sql.catalyst.expressions.Cast | smallint | N/A | N/A | | org.apache.spark.sql.catalyst.expressions.Cast | string | N/A | N/A | +| org.apache.spark.sql.catalyst.expressions.Cast | time | N/A | N/A | | org.apache.spark.sql.catalyst.expressions.Cast | timestamp | N/A | N/A | | org.apache.spark.sql.catalyst.expressions.Cast | tinyint | N/A | N/A | | org.apache.spark.sql.catalyst.expressions.Cbrt | cbrt | SELECT cbrt(27.0) | struct<CBRT(27.0):double> | @@ -480,4 +481,4 @@ | org.apache.spark.sql.catalyst.expressions.xml.XPathList | xpath | SELECT xpath('<a><b>b1</b><b>b2</b><b>b3</b><c>c1</c><c>c2</c></a>','a/b/text()') | struct<xpath(<a><b>b1</b><b>b2</b><b>b3</b><c>c1</c><c>c2</c></a>, a/b/text()):array<string>> | | org.apache.spark.sql.catalyst.expressions.xml.XPathLong | xpath_long | SELECT xpath_long('<a><b>1</b><b>2</b></a>', 'sum(a/b)') | struct<xpath_long(<a><b>1</b><b>2</b></a>, sum(a/b)):bigint> | | org.apache.spark.sql.catalyst.expressions.xml.XPathShort | xpath_short | SELECT xpath_short('<a><b>1</b><b>2</b></a>', 'sum(a/b)') | struct<xpath_short(<a><b>1</b><b>2</b></a>, sum(a/b)):smallint> | -| org.apache.spark.sql.catalyst.expressions.xml.XPathString | xpath_string | SELECT xpath_string('<a><b>b</b><c>cc</c></a>','a/c') | struct<xpath_string(<a><b>b</b><c>cc</c></a>, a/c):string> | +| org.apache.spark.sql.catalyst.expressions.xml.XPathString | xpath_string | SELECT xpath_string('<a><b>b</b><c>cc</c></a>','a/c') | struct<xpath_string(<a><b>b</b><c>cc</c></a>, a/c):string> | \ No newline at end of file diff --git a/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out b/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out index c536fbcbe5b2..df6cef2f62f8 100644 --- a/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out +++ b/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out @@ -401,6 +401,34 @@ Project [extract(SECOND, cast(09:08:01.987654 as time(6))) AS extract(SECOND FRO +- OneRowRelation +-- !query +SELECT cast("12:34:56" as time) +-- !query analysis +Project [cast(12:34:56 as time(6)) AS CAST(12:34:56 AS TIME(6))#x] ++- OneRowRelation + + +-- !query +SELECT cast("12:34:56.789" as time(3)) +-- !query analysis +Project [cast(12:34:56.789 as time(3)) AS CAST(12:34:56.789 AS TIME(3))#x] ++- OneRowRelation + + +-- !query +SELECT cast("12:34:56.789" as time(6)) +-- !query analysis +Project [cast(12:34:56.789 as time(6)) AS CAST(12:34:56.789 AS TIME(6))#x] ++- OneRowRelation + + +-- !query +SELECT cast("12:34:56.789012" as time without time zone) +-- !query analysis +Project [cast(12:34:56.789012 as time(6)) AS CAST(12:34:56.789012 AS TIME(6))#x] ++- OneRowRelation + + -- !query SELECT cast(cast('12:00' as time(0)) as time(2)) -- !query analysis @@ -422,6 +450,48 @@ Project [cast(11:59:59.999999 as time(6)) AS CAST(TIME '11:59:59.999999' AS TIME +- OneRowRelation +-- !query +SELECT time("12:34:56") +-- !query analysis +Project [cast(12:34:56 as time(6)) AS 12:34:56#x] ++- OneRowRelation + + +-- !query +SELECT time("12:34:56.789") +-- !query analysis +Project [cast(12:34:56.789 as time(6)) AS 12:34:56.789#x] ++- OneRowRelation + + +-- !query +SELECT time("12:34:56.789012") +-- !query analysis +Project [cast(12:34:56.789012 as time(6)) AS 12:34:56.789012#x] ++- OneRowRelation + + +-- !query +SELECT time(cast('12:00' as time(0))) +-- !query analysis +Project [cast(cast(12:00 as time(0)) as time(6)) AS CAST(12:00 AS TIME(0))#x] ++- OneRowRelation + + +-- !query +SELECT time(('23:59:59.001001' :: time(6))) +-- !query analysis +Project [cast(cast(23:59:59.001001 as time(6)) as time(6)) AS CAST(23:59:59.001001 AS TIME(6))#x] ++- OneRowRelation + + +-- !query +SELECT time(time'11:59:59.999999') +-- !query analysis +Project [cast(11:59:59.999999 as time(6)) AS TIME '11:59:59.999999'#x] ++- OneRowRelation + + -- !query SELECT '12:43:33.1234' :: TIME(4) + INTERVAL '01:04:05.56' HOUR TO SECOND -- !query analysis diff --git a/sql/core/src/test/resources/sql-tests/inputs/time.sql b/sql/core/src/test/resources/sql-tests/inputs/time.sql index 5a6569f4a6f8..6fced5cc2f51 100644 --- a/sql/core/src/test/resources/sql-tests/inputs/time.sql +++ b/sql/core/src/test/resources/sql-tests/inputs/time.sql @@ -68,11 +68,25 @@ select extract(SECOND FROM cast('09:08:01.987654' as time(4))); select extract(SECOND FROM cast('09:08:01.987654' as time(5))); select extract(SECOND FROM cast('09:08:01.987654' as time(6))); +-- cast string to time +SELECT cast("12:34:56" as time); +SELECT cast("12:34:56.789" as time(3)); +SELECT cast("12:34:56.789" as time(6)); +SELECT cast("12:34:56.789012" as time without time zone); + -- cast time to time SELECT cast(cast('12:00' as time(0)) as time(2)); SELECT cast(('23:59:59.001001' :: time(6)) as time(4)); SELECT cast(time'11:59:59.999999' as time without time zone); +-- SPARK-51562: test time function (i.e. alias for casting to time type). +SELECT time("12:34:56"); +SELECT time("12:34:56.789"); +SELECT time("12:34:56.789012"); +SELECT time(cast('12:00' as time(0))); +SELECT time(('23:59:59.001001' :: time(6))); +SELECT time(time'11:59:59.999999'); + -- +/- ANSI day-time intervals SELECT '12:43:33.1234' :: TIME(4) + INTERVAL '01:04:05.56' HOUR TO SECOND; SELECT TIME'08:30' + NULL; diff --git a/sql/core/src/test/resources/sql-tests/results/time.sql.out b/sql/core/src/test/resources/sql-tests/results/time.sql.out index 29ef0816c316..4a87c026e6df 100644 --- a/sql/core/src/test/resources/sql-tests/results/time.sql.out +++ b/sql/core/src/test/resources/sql-tests/results/time.sql.out @@ -500,6 +500,38 @@ struct<extract(SECOND FROM CAST(09:08:01.987654 AS TIME(6))):decimal(8,6)> 1.987654 +-- !query +SELECT cast("12:34:56" as time) +-- !query schema +struct<CAST(12:34:56 AS TIME(6)):time(6)> +-- !query output +12:34:56 + + +-- !query +SELECT cast("12:34:56.789" as time(3)) +-- !query schema +struct<CAST(12:34:56.789 AS TIME(3)):time(3)> +-- !query output +12:34:56.789 + + +-- !query +SELECT cast("12:34:56.789" as time(6)) +-- !query schema +struct<CAST(12:34:56.789 AS TIME(6)):time(6)> +-- !query output +12:34:56.789 + + +-- !query +SELECT cast("12:34:56.789012" as time without time zone) +-- !query schema +struct<CAST(12:34:56.789012 AS TIME(6)):time(6)> +-- !query output +12:34:56.789012 + + -- !query SELECT cast(cast('12:00' as time(0)) as time(2)) -- !query schema @@ -524,6 +556,54 @@ struct<CAST(TIME '11:59:59.999999' AS TIME(6)):time(6)> 11:59:59.999999 +-- !query +SELECT time("12:34:56") +-- !query schema +struct<12:34:56:time(6)> +-- !query output +12:34:56 + + +-- !query +SELECT time("12:34:56.789") +-- !query schema +struct<12:34:56.789:time(6)> +-- !query output +12:34:56.789 + + +-- !query +SELECT time("12:34:56.789012") +-- !query schema +struct<12:34:56.789012:time(6)> +-- !query output +12:34:56.789012 + + +-- !query +SELECT time(cast('12:00' as time(0))) +-- !query schema +struct<CAST(12:00 AS TIME(0)):time(6)> +-- !query output +12:00:00 + + +-- !query +SELECT time(('23:59:59.001001' :: time(6))) +-- !query schema +struct<CAST(23:59:59.001001 AS TIME(6)):time(6)> +-- !query output +23:59:59.001001 + + +-- !query +SELECT time(time'11:59:59.999999') +-- !query schema +struct<TIME '11:59:59.999999':time(6)> +-- !query output +11:59:59.999999 + + -- !query SELECT '12:43:33.1234' :: TIME(4) + INTERVAL '01:04:05.56' HOUR TO SECOND -- !query schema --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@spark.apache.org For additional commands, e-mail: commits-h...@spark.apache.org