diff --git a/docs/sql-ref-ansi-compliance.md b/docs/sql-ref-ansi-compliance.md index d28f0af5dd0af..142cd1734431b 100644 --- a/docs/sql-ref-ansi-compliance.md +++ b/docs/sql-ref-ansi-compliance.md @@ -362,6 +362,7 @@ The behavior of some SQL functions can be different under ANSI mode (`spark.sql. - `unix_timestamp`: This function should fail with an exception if the input string can't be parsed, or the pattern string is invalid. - `to_unix_timestamp`: This function should fail with an exception if the input string can't be parsed, or the pattern string is invalid. - `make_date`: This function should fail with an exception if the result date is invalid. + - `make_time`: This function should fail with an exception if the result time is invalid. - `make_timestamp`: This function should fail with an exception if the result timestamp is invalid. - `make_interval`: This function should fail with an exception if the result interval is invalid. - `next_day`: This function throws `IllegalArgumentException` if input is not a valid day of week. @@ -388,6 +389,7 @@ When ANSI mode is on, it throws exceptions for invalid operations. You can use t - `try_make_timestamp`: identical to the function `make_timestamp`, except that it returns `NULL` result instead of throwing an exception on error. - `try_make_timestamp_ltz`: identical to the function `make_timestamp_ltz`, except that it returns `NULL` result instead of throwing an exception on error. - `try_make_timestamp_ntz`: identical to the function `make_timestamp_ntz`, except that it returns `NULL` result instead of throwing an exception on error. + - `try_make_time`: identical to the function `make_time`, except that it returns `NULL` result instead of throwing an exception on invalid inputs. - `try_make_interval`: identical to the function `make_interval`, except that it returns `NULL` result instead of throwing an exception on invalid interval. - `try_to_time`: identical to the function `to_time`, except that it returns `NULL` result instead of throwing an exception on string parsing error. - `try_to_date`: identical to the function `to_date`, except that it returns `NULL` result instead of throwing an exception on string parsing error. diff --git a/python/docs/source/reference/pyspark.sql/functions.rst b/python/docs/source/reference/pyspark.sql/functions.rst index 45e0eaff0296e..4189ec17d8b72 100644 --- a/python/docs/source/reference/pyspark.sql/functions.rst +++ b/python/docs/source/reference/pyspark.sql/functions.rst @@ -319,6 +319,7 @@ Date and Timestamp Functions to_utc_timestamp trunc try_make_interval + try_make_time try_make_timestamp try_make_timestamp_ltz try_make_timestamp_ntz diff --git a/python/pyspark/sql/connect/functions/builtin.py b/python/pyspark/sql/connect/functions/builtin.py index 7a4d2b0e1b6c4..2c40afe92afcb 100644 --- a/python/pyspark/sql/connect/functions/builtin.py +++ b/python/pyspark/sql/connect/functions/builtin.py @@ -4085,6 +4085,13 @@ def make_time(hour: "ColumnOrName", minute: "ColumnOrName", second: "ColumnOrNam make_time.__doc__ = pysparkfuncs.make_time.__doc__ +def try_make_time(hour: "ColumnOrName", minute: "ColumnOrName", second: "ColumnOrName") -> Column: + return _invoke_function_over_columns("try_make_time", hour, minute, second) + + +try_make_time.__doc__ = pysparkfuncs.try_make_time.__doc__ + + def time_from_seconds(col: "ColumnOrName") -> Column: return _invoke_function_over_columns("time_from_seconds", col) diff --git a/python/pyspark/sql/functions/__init__.py b/python/pyspark/sql/functions/__init__.py index 5083d5c0db16b..21559cb1c63b5 100644 --- a/python/pyspark/sql/functions/__init__.py +++ b/python/pyspark/sql/functions/__init__.py @@ -268,6 +268,7 @@ "to_utc_timestamp", "trunc", "try_make_interval", + "try_make_time", "try_make_timestamp", "try_make_timestamp_ltz", "try_make_timestamp_ntz", diff --git a/python/pyspark/sql/functions/builtin.py b/python/pyspark/sql/functions/builtin.py index 44e483ecfb6bd..66531dc00569a 100644 --- a/python/pyspark/sql/functions/builtin.py +++ b/python/pyspark/sql/functions/builtin.py @@ -25580,6 +25580,54 @@ def make_time(hour: "ColumnOrName", minute: "ColumnOrName", second: "ColumnOrNam return _invoke_function_over_columns("make_time", hour, minute, second) +@_try_remote_functions +def try_make_time(hour: "ColumnOrName", minute: "ColumnOrName", second: "ColumnOrName") -> Column: + """ + Try to create time from hour, minute and second fields. + The function returns NULL on invalid inputs. + + .. versionadded:: 4.1.0 + + Parameters + ---------- + hour : :class:`~pyspark.sql.Column` or column name + The hour to represent, from 0 to 23. + minute : :class:`~pyspark.sql.Column` or column name + The minute to represent, from 0 to 59. + second : :class:`~pyspark.sql.Column` or column name + The second to represent, from 0 to 59.999999. + + Returns + ------- + :class:`~pyspark.sql.Column` + A column representing the created time, or NULL in case of an error. + + See Also + -------- + :meth:`pyspark.sql.functions.make_time` + + Examples + -------- + >>> from pyspark.sql import functions as sf + >>> df = spark.createDataFrame([(6, 30, 45.887)], ["hour", "minute", "second"]) + >>> df.select(sf.try_make_time("hour", "minute", "second")).show() + +-----------------------------------+ + |try_make_time(hour, minute, second)| + +-----------------------------------+ + | 06:30:45.887| + +-----------------------------------+ + + >>> df = spark.createDataFrame([(25, 30, 0.0)], ["hour", "minute", "second"]) + >>> df.select(sf.try_make_time("hour", "minute", "second")).show() + +-----------------------------------+ + |try_make_time(hour, minute, second)| + +-----------------------------------+ + | NULL| + +-----------------------------------+ + """ + return _invoke_function_over_columns("try_make_time", hour, minute, second) + + @_try_remote_functions def time_from_seconds(col: "ColumnOrName") -> Column: """ diff --git a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala index 0dc667a00f6d9..01388f892c37b 100644 --- a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala +++ b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala @@ -890,6 +890,23 @@ object functions { Column.fn("make_time", hour, minute, second) } + /** + * Try to create time from hour, minute and second fields. The function returns NULL on invalid + * inputs. + * + * @param hour + * the hour to represent, from 0 to 23 + * @param minute + * the minute to represent, from 0 to 59 + * @param second + * the second to represent, from 0 to 59.999999 + * @group datetime_funcs + * @since 4.1.0 + */ + def try_make_time(hour: Column, minute: Column, second: Column): Column = { + Column.fn("try_make_time", hour, minute, second) + } + /** * Aggregate function: returns the most frequent value in a group. * 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 a8654491a2697..4f0f524c024b0 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 @@ -746,6 +746,7 @@ object FunctionRegistry { expression[WindowTime]("window_time"), expression[MakeDate]("make_date"), expression[MakeTime]("make_time"), + expressionBuilder("try_make_time", TryMakeTimeExpressionBuilder), expression[TimeTrunc]("time_trunc"), expression[TimeFromSeconds]("time_from_seconds"), expression[TimeFromMillis]("time_from_millis"), diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala index 1c9d6b335e8d9..034c0a9300e36 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala @@ -623,6 +623,57 @@ case class MakeTime( copy(hours = newChildren(0), minutes = newChildren(1), secsAndMicros = newChildren(2)) } +// scalastyle:off line.size.limit +@ExpressionDescription( + usage = "_FUNC_(hour, minute, second) - Try to create time from hour, minute and second fields. The function returns NULL on invalid inputs.", + arguments = """ + Arguments: + * hour - the hour to represent, from 0 to 23 + * minute - the minute to represent, from 0 to 59 + * second - the second to represent, from 0 to 59.999999 + """, + examples = """ + Examples: + > SELECT _FUNC_(6, 30, 45.887); + 06:30:45.887 + > SELECT _FUNC_(NULL, 30, 0); + NULL + > SELECT _FUNC_(25, 30, 0); + NULL + """, + group = "datetime_funcs", + since = "4.1.0") +// scalastyle:on line.size.limit +object TryMakeTimeExpressionBuilder extends ExpressionBuilder { + override def build(funcName: String, expressions: Seq[Expression]): Expression = { + val numArgs = expressions.length + if (numArgs == 3) { + new TryMakeTime(expressions(0), expressions(1), expressions(2)) + } else { + throw QueryCompilationErrors.wrongNumArgsError(funcName, Seq(3), numArgs) + } + } +} + +case class TryMakeTime( + hours: Expression, + minutes: Expression, + secsAndMicros: Expression, + replacement: Expression) + extends RuntimeReplaceable with InheritAnalysisRules { + + def this(hours: Expression, minutes: Expression, secsAndMicros: Expression) = + this(hours, minutes, secsAndMicros, + TryEval(MakeTime(hours, minutes, secsAndMicros))) + + override def prettyName: String = "try_make_time" + + override def parameters: Seq[Expression] = Seq(hours, minutes, secsAndMicros) + + override protected def withNewChildInternal(newChild: Expression): TryMakeTime = + copy(replacement = newChild) +} + /** * Adds day-time interval to time. */ diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimeExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimeExpressionsSuite.scala index 6db6115a1e5e8..1b5fab77a6d44 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimeExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimeExpressionsSuite.scala @@ -233,6 +233,44 @@ class TimeExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { ) } + test("creating values of TimeType via try_make_time") { + // Valid cases should return the same result as make_time + checkEvaluation( + TryEval(MakeTime(Literal(13), Literal(2), Literal(Decimal(23.5, 16, 6)))), + LocalTime.of(13, 2, 23, 500000000)) + checkEvaluation( + TryEval(MakeTime(Literal(0), Literal(0), Literal(Decimal(0.0, 16, 6)))), + LocalTime.of(0, 0, 0, 0)) + checkEvaluation( + TryEval(MakeTime(Literal(23), Literal(59), Literal(Decimal(59.999999, 16, 6)))), + LocalTime.of(23, 59, 59, 999999000)) + + // Null cases + checkEvaluation( + TryEval(MakeTime( + Literal.create(null, IntegerType), Literal(18), Literal(Decimal(23.5, 16, 6)))), + null) + checkEvaluation( + TryEval(MakeTime( + Literal(13), Literal.create(null, IntegerType), Literal(Decimal(23.5, 16, 6)))), + null) + checkEvaluation( + TryEval(MakeTime( + Literal(13), Literal(18), Literal.create(null, DecimalType(16, 6)))), + null) + + // Invalid cases return null instead of throwing + checkEvaluation( + TryEval(MakeTime(Literal(25), Literal(2), Literal(Decimal(23.5, 16, 6)))), + null) + checkEvaluation( + TryEval(MakeTime(Literal(23), Literal(-1), Literal(Decimal(23.5, 16, 6)))), + null) + checkEvaluation( + TryEval(MakeTime(Literal(23), Literal(12), Literal(Decimal(100.5, 16, 6)))), + null) + } + test("SecondExpressionBuilder") { // Empty expressions list checkError( diff --git a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala index a74b25459bad2..8f87b5673b150 100644 --- a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala +++ b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala @@ -2372,6 +2372,10 @@ class PlanGenerationTestSuite extends ConnectFunSuite with Logging { fn.make_time(fn.lit(12), fn.lit(13), fn.lit(14)) } + temporalFunctionTest("try_make_time") { + fn.try_make_time(fn.lit(12), fn.lit(13), fn.lit(14)) + } + temporalFunctionTest("current_time") { fn.current_time() } diff --git a/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_make_time.explain b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_make_time.explain new file mode 100644 index 0000000000000..3c7cff4ea0ec4 --- /dev/null +++ b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_make_time.explain @@ -0,0 +1,2 @@ +Project [tryeval(static_invoke(DateTimeUtils.makeTime(12, 13, cast(14 as decimal(16,6))))) AS try_make_time(12, 13, 14)#0] ++- LocalRelation , [d#0, t#0, s#0, x#0L, wt#0] diff --git a/sql/connect/common/src/test/resources/query-tests/queries/function_try_make_time.json b/sql/connect/common/src/test/resources/query-tests/queries/function_try_make_time.json new file mode 100644 index 0000000000000..9aecb38497e71 --- /dev/null +++ b/sql/connect/common/src/test/resources/query-tests/queries/function_try_make_time.json @@ -0,0 +1,102 @@ +{ + "common": { + "planId": "1" + }, + "project": { + "input": { + "common": { + "planId": "0" + }, + "localRelation": { + "schema": "struct\u003cd:date,t:timestamp,s:string,x:bigint,wt:struct\u003cstart:timestamp,end:timestamp\u003e\u003e" + } + }, + "expressions": [{ + "unresolvedFunction": { + "functionName": "try_make_time", + "arguments": [{ + "literal": { + "integer": 12 + }, + "common": { + "origin": { + "jvmOrigin": { + "stackTrace": [{ + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.functions$", + "methodName": "lit", + "fileName": "functions.scala" + }, { + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite", + "methodName": "~~trimmed~anonfun~~", + "fileName": "PlanGenerationTestSuite.scala" + }] + } + } + } + }, { + "literal": { + "integer": 13 + }, + "common": { + "origin": { + "jvmOrigin": { + "stackTrace": [{ + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.functions$", + "methodName": "lit", + "fileName": "functions.scala" + }, { + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite", + "methodName": "~~trimmed~anonfun~~", + "fileName": "PlanGenerationTestSuite.scala" + }] + } + } + } + }, { + "literal": { + "integer": 14 + }, + "common": { + "origin": { + "jvmOrigin": { + "stackTrace": [{ + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.functions$", + "methodName": "lit", + "fileName": "functions.scala" + }, { + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite", + "methodName": "~~trimmed~anonfun~~", + "fileName": "PlanGenerationTestSuite.scala" + }] + } + } + } + }], + "isInternal": false + }, + "common": { + "origin": { + "jvmOrigin": { + "stackTrace": [{ + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.functions$", + "methodName": "try_make_time", + "fileName": "functions.scala" + }, { + "classLoaderName": "app", + "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite", + "methodName": "~~trimmed~anonfun~~", + "fileName": "PlanGenerationTestSuite.scala" + }] + } + } + } + }] + } +} \ No newline at end of file diff --git a/sql/connect/common/src/test/resources/query-tests/queries/function_try_make_time.proto.bin b/sql/connect/common/src/test/resources/query-tests/queries/function_try_make_time.proto.bin new file mode 100644 index 0000000000000..3acea465aadc8 Binary files /dev/null and b/sql/connect/common/src/test/resources/query-tests/queries/function_try_make_time.proto.bin differ 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 5134975689e3e..200e2848cf23f 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 @@ -395,6 +395,7 @@ | org.apache.spark.sql.catalyst.expressions.TryDivide | try_divide | SELECT try_divide(3, 2) | struct | | org.apache.spark.sql.catalyst.expressions.TryElementAt | try_element_at | SELECT try_element_at(array(1, 2, 3), 2) | struct | | org.apache.spark.sql.catalyst.expressions.TryMakeInterval | try_make_interval | SELECT try_make_interval(100, 11, 1, 1, 12, 30, 01.001001) | struct | +| org.apache.spark.sql.catalyst.expressions.TryMakeTimeExpressionBuilder | try_make_time | SELECT try_make_time(6, 30, 45.887) | struct | | org.apache.spark.sql.catalyst.expressions.TryMakeTimestampExpressionBuilder | try_make_timestamp | SELECT try_make_timestamp(2014, 12, 28, 6, 30, 45.887) | struct | | org.apache.spark.sql.catalyst.expressions.TryMakeTimestampLTZExpressionBuilder | try_make_timestamp_ltz | SELECT try_make_timestamp_ltz(2014, 12, 28, 6, 30, 45.887) | struct | | org.apache.spark.sql.catalyst.expressions.TryMakeTimestampNTZExpressionBuilder | try_make_timestamp_ntz | SELECT try_make_timestamp_ntz(2014, 12, 28, 6, 30, 45.887) | struct | 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 d5d5700316f86..2dc38a7b721d6 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 @@ -207,6 +207,90 @@ Project [make_time(1, 18, 4294967297.999999) AS make_time(1, 18, 4294967297.9999 +- OneRowRelation +-- !query +select try_make_time(6, 30, 45.887) +-- !query analysis +Project [try_make_time(6, 30, 45.887) AS try_make_time(6, 30, 45.887)#x] ++- OneRowRelation + + +-- !query +select try_make_time(0, 0, 0.0) +-- !query analysis +Project [try_make_time(0, 0, 0.0) AS try_make_time(0, 0, 0.0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(23, 59, 59.999999) +-- !query analysis +Project [try_make_time(23, 59, 59.999999) AS try_make_time(23, 59, 59.999999)#x] ++- OneRowRelation + + +-- !query +select try_make_time(null, 30, 0) +-- !query analysis +Project [try_make_time(null, 30, 0) AS try_make_time(NULL, 30, 0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(1, null, 0) +-- !query analysis +Project [try_make_time(1, null, 0) AS try_make_time(1, NULL, 0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(1, 30, null) +-- !query analysis +Project [try_make_time(1, 30, null) AS try_make_time(1, 30, NULL)#x] ++- OneRowRelation + + +-- !query +select try_make_time(25, 30, 0) +-- !query analysis +Project [try_make_time(25, 30, 0) AS try_make_time(25, 30, 0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(-1, 30, 0) +-- !query analysis +Project [try_make_time(-1, 30, 0) AS try_make_time(-1, 30, 0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(1, 60, 0) +-- !query analysis +Project [try_make_time(1, 60, 0) AS try_make_time(1, 60, 0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(1, -1, 0) +-- !query analysis +Project [try_make_time(1, -1, 0) AS try_make_time(1, -1, 0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(1, 18, 60.0) +-- !query analysis +Project [try_make_time(1, 18, 60.0) AS try_make_time(1, 18, 60.0)#x] ++- OneRowRelation + + +-- !query +select try_make_time(1, 18, -1.0) +-- !query analysis +Project [try_make_time(1, 18, -1.0) AS try_make_time(1, 18, -1.0)#x] ++- OneRowRelation + + -- !query select second(to_time('23-59-58.987654', 'HH-mm-ss.SSSSSS')) -- !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 1a836b270edc0..3b13d4f9c637d 100644 --- a/sql/core/src/test/resources/sql-tests/inputs/time.sql +++ b/sql/core/src/test/resources/sql-tests/inputs/time.sql @@ -36,6 +36,22 @@ select make_time(1, 18, -999999999.999999); -- Full seconds overflows to a valid seconds integer when converted from long to int select make_time(1, 18, 4294967297.999999); +-- try_make_time: valid cases +select try_make_time(6, 30, 45.887); +select try_make_time(0, 0, 0.0); +select try_make_time(23, 59, 59.999999); +-- try_make_time: null inputs +select try_make_time(null, 30, 0); +select try_make_time(1, null, 0); +select try_make_time(1, 30, null); +-- try_make_time: invalid inputs return NULL +select try_make_time(25, 30, 0); +select try_make_time(-1, 30, 0); +select try_make_time(1, 60, 0); +select try_make_time(1, -1, 0); +select try_make_time(1, 18, 60.0); +select try_make_time(1, 18, -1.0); + select second(to_time('23-59-58.987654', 'HH-mm-ss.SSSSSS')); select minute(to_time('23-59-58.987654', 'HH-mm-ss.SSSSSS')); select hour(to_time('23-59-58.987654', 'HH-mm-ss.SSSSSS')); 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 dace78ab938d3..eb7cd22e867f7 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 @@ -276,6 +276,102 @@ org.apache.spark.SparkDateTimeException } +-- !query +select try_make_time(6, 30, 45.887) +-- !query schema +struct +-- !query output +06:30:45.887 + + +-- !query +select try_make_time(0, 0, 0.0) +-- !query schema +struct +-- !query output +00:00:00 + + +-- !query +select try_make_time(23, 59, 59.999999) +-- !query schema +struct +-- !query output +23:59:59.999999 + + +-- !query +select try_make_time(null, 30, 0) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(1, null, 0) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(1, 30, null) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(25, 30, 0) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(-1, 30, 0) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(1, 60, 0) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(1, -1, 0) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(1, 18, 60.0) +-- !query schema +struct +-- !query output +NULL + + +-- !query +select try_make_time(1, 18, -1.0) +-- !query schema +struct +-- !query output +NULL + + -- !query select second(to_time('23-59-58.987654', 'HH-mm-ss.SSSSSS')) -- !query schema diff --git a/sql/core/src/test/scala/org/apache/spark/sql/TimeFunctionsSuiteBase.scala b/sql/core/src/test/scala/org/apache/spark/sql/TimeFunctionsSuiteBase.scala index 0766c9fcc3c47..8c8803153d185 100644 --- a/sql/core/src/test/scala/org/apache/spark/sql/TimeFunctionsSuiteBase.scala +++ b/sql/core/src/test/scala/org/apache/spark/sql/TimeFunctionsSuiteBase.scala @@ -164,6 +164,49 @@ abstract class TimeFunctionsSuiteBase extends SharedSparkSession { checkAnswer(result2, expected) } + test("SPARK-57846: try_make_time function") { + // Valid input data. + val schema = StructType(Seq( + StructField("hour", IntegerType, nullable = false), + StructField("minute", IntegerType, nullable = false), + StructField("second", DecimalType(16, 6), nullable = false) + )) + val data = Seq( + Row(0, 0, BigDecimal(0.0)), + Row(1, 2, BigDecimal(3.4)), + Row(23, 59, BigDecimal(59.999999)) + ) + val df = spark.createDataFrame(spark.sparkContext.parallelize(data), schema) + + // Test valid inputs using both selectExpr and select. + val result1 = df.selectExpr("try_make_time(hour, minute, second)") + val result2 = df.select(try_make_time(col("hour"), col("minute"), col("second"))) + checkAnswer(result1, result2) + + val expected = Seq( + "00:00:00", + "01:02:03.4", + "23:59:59.999999" + ).toDF("timeString").select(col("timeString").cast("time")) + checkAnswer(result1, expected) + + // Test invalid inputs return null instead of throwing. + val invalidSchema = StructType(Seq( + StructField("hour", IntegerType, nullable = false), + StructField("minute", IntegerType, nullable = false), + StructField("second", DecimalType(16, 6), nullable = false) + )) + val invalidData = Seq( + Row(25, 0, BigDecimal(0.0)), + Row(1, 60, BigDecimal(0.0)), + Row(1, 2, BigDecimal(60.0)) + ) + val invalidDf = spark.createDataFrame( + spark.sparkContext.parallelize(invalidData), invalidSchema) + val invalidResult = invalidDf.selectExpr("try_make_time(hour, minute, second)") + checkAnswer(invalidResult, Seq(Row(null), Row(null), Row(null))) + } + test("SPARK-53929: make_timestamp function") { // Input data for the function. val schema = StructType(Seq(