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 fc3191b1f4a8 [SPARK-57558][SQL] Add the LOCALTIME datetime value 
function
fc3191b1f4a8 is described below

commit fc3191b1f4a833b47c638f229a078b4cd57ef2e9
Author: Shrirang Mhalgi <[email protected]>
AuthorDate: Fri Jun 26 12:25:39 2026 +0200

    [SPARK-57558][SQL] Add the LOCALTIME datetime value function
    
    ### What changes were proposed in this pull request?
    
    Add the ANSI `LOCALTIME` datetime value function as an alias for 
`current_time`, returning the current local time as `TimeType` with optional 
precision.
    
    ### Why are the changes needed?
    
    ANSI SQL defines `LOCALTIME` (TIME WITHOUT TIME ZONE) alongside 
`CURRENT_TIME`. Spark has `localtimestamp` but not `localtime`. Since Spark's 
`TIME` is always WITHOUT TIME ZONE, `LOCALTIME` is the standard-named 
counterpart.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `SELECT LOCALTIME`, `SELECT LOCALTIME()`, and `SELECT localtime(3)` 
now return the current time at the requested precision.
    
    ### How was this patch tested?
    
    - Added parser test: `LOCALTIME` resolves to CurrentTime() in ANSI mode, 
UnresolvedAttribute in non-ANSI mode (ExpressionParserSuite)
    - Added end-to-end test: `localtime()` returns `TimeType(6)`, 
`localtime(3)` returns `TimeType(3)`, `localtime() equals current_time()`
    - All existing tests pass (ExpressionParserSuite: 50/50, 
`TimeFunctionsSuiteBase (via TimeFunctionsAnsiOnSuite/AnsiOffSuite)`)
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Co-Authored using Claude Opus 4.6
    
    Closes #56766 from shrirangmhalgi/SPARK-57558-add-localtime-function.
    
    Authored-by: Shrirang Mhalgi <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 docs/sql-ref-ansi-compliance.md                     |  1 +
 .../spark/sql/catalyst/parser/SqlBaseLexer.g4       |  1 +
 .../spark/sql/catalyst/parser/SqlBaseParser.g4      |  3 ++-
 .../sql/catalyst/analysis/FunctionRegistry.scala    |  1 +
 .../analysis/LiteralFunctionResolution.scala        |  1 +
 .../spark/sql/catalyst/parser/AstBuilder.scala      |  2 +-
 .../sql/catalyst/parser/ExpressionParserSuite.scala |  2 ++
 .../sql-functions/sql-expression-schema.md          |  1 +
 .../sql-tests/results/keywords-enforced.sql.out     |  2 ++
 .../resources/sql-tests/results/keywords.sql.out    |  1 +
 .../sql-tests/results/nonansi/keywords.sql.out      |  1 +
 .../apache/spark/sql/TimeFunctionsSuiteBase.scala   | 21 +++++++++++++++++++++
 .../ThriftServerWithSparkContextSuite.scala         |  2 +-
 13 files changed, 36 insertions(+), 3 deletions(-)

diff --git a/docs/sql-ref-ansi-compliance.md b/docs/sql-ref-ansi-compliance.md
index ea89e8cea349..d28f0af5dd0a 100644
--- a/docs/sql-ref-ansi-compliance.md
+++ b/docs/sql-ref-ansi-compliance.md
@@ -628,6 +628,7 @@ Below is a list of all the keywords in Spark SQL.
 |LIST|non-reserved|non-reserved|non-reserved|
 |LOAD|non-reserved|non-reserved|non-reserved|
 |LOCAL|non-reserved|non-reserved|reserved|
+|LOCALTIME|reserved|non-reserved|reserved|
 |LOCATION|non-reserved|non-reserved|non-reserved|
 |LOCK|non-reserved|non-reserved|non-reserved|
 |LOCKS|non-reserved|non-reserved|non-reserved|
diff --git 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
index bf759e501db9..0e940ee5b4b0 100644
--- 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
+++ 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
@@ -345,6 +345,7 @@ LINES: 'LINES';
 LIST: 'LIST';
 LOAD: 'LOAD';
 LOCAL: 'LOCAL';
+LOCALTIME: 'LOCALTIME';
 LOCATION: 'LOCATION';
 LOCK: 'LOCK';
 LOCKS: 'LOCKS';
diff --git 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
index cce03c169aac..2466cf62272a 100644
--- 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
+++ 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
@@ -1338,7 +1338,7 @@ datetimeUnit
     ;
 
 primaryExpression
-    : name=(CURRENT_DATE | CURRENT_TIMESTAMP | CURRENT_USER | USER | 
SESSION_USER | CURRENT_TIME | CURRENT_PATH)             #currentLike
+    : name=(CURRENT_DATE | CURRENT_TIMESTAMP | CURRENT_USER | USER | 
SESSION_USER | CURRENT_TIME | CURRENT_PATH | LOCALTIME)             #currentLike
     | name=(TIMESTAMPADD | DATEADD | DATE_ADD) LEFT_PAREN (unit=datetimeUnit | 
invalidUnit=stringLit) COMMA unitsAmount=valueExpression COMMA 
timestamp=valueExpression RIGHT_PAREN             #timestampadd
     | name=(TIMESTAMPDIFF | DATEDIFF | DATE_DIFF | TIMEDIFF) LEFT_PAREN 
(unit=datetimeUnit | invalidUnit=stringLit) COMMA 
startTimestamp=valueExpression COMMA endTimestamp=valueExpression RIGHT_PAREN   
 #timestampdiff
     | CASE whenClause+ (ELSE elseExpression=expression)? END                   
                #searchedCase
@@ -2545,6 +2545,7 @@ nonReserved
     | LIST
     | LOAD
     | LOCAL
+    | LOCALTIME
     | LOCATION
     | LOCK
     | LOCKS
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 415a842c9bf4..737a9da8b2b7 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
@@ -700,6 +700,7 @@ object FunctionRegistry {
     expressionBuilder("curdate", CurDateExpressionBuilder, setAlias = true),
     expression[CurrentTimestamp]("current_timestamp"),
     expression[CurrentTime]("current_time"),
+    expression[CurrentTime]("localtime", since = Some("4.3.0")),
     expression[CurrentTimeZone]("current_timezone"),
     expression[LocalTimestamp]("localtimestamp"),
     expression[DateDiff]("datediff"),
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/LiteralFunctionResolution.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/LiteralFunctionResolution.scala
index 7483adb2dbba..6f09aeff0bdf 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/LiteralFunctionResolution.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/LiteralFunctionResolution.scala
@@ -44,6 +44,7 @@ object LiteralFunctionResolution {
     (CurrentDate().prettyName, () => CurrentDate(), e => toPrettySQL(e)),
     (CurrentTimestamp().prettyName, () => CurrentTimestamp(), e => 
toPrettySQL(e)),
     (CurrentTime().prettyName, () => CurrentTime(), e => toPrettySQL(e)),
+    ("localtime", () => CurrentTime(), e => toPrettySQL(e)),
     (CurrentUser().prettyName, () => CurrentUser(), e => toPrettySQL(e)),
     ("user", () => CurrentUser(), e => toPrettySQL(e)),
     ("session_user", () => CurrentUser(), e => toPrettySQL(e)),
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
index 43e094b5ccec..fe4841042a39 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
@@ -3588,7 +3588,7 @@ class AstBuilder extends DataTypeAstBuilder
           CurrentDate()
         case SqlBaseParser.CURRENT_TIMESTAMP =>
           CurrentTimestamp()
-        case SqlBaseParser.CURRENT_TIME =>
+        case SqlBaseParser.CURRENT_TIME | SqlBaseParser.LOCALTIME =>
           CurrentTime()
         case SqlBaseParser.CURRENT_PATH =>
           CurrentPath()
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/ExpressionParserSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/ExpressionParserSuite.scala
index 30bed5ca7649..3812dd287837 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/ExpressionParserSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/ExpressionParserSuite.scala
@@ -1322,12 +1322,14 @@ class ExpressionParserSuite extends AnalysisTest {
       assertEqual("current_date", CurrentDate())
       assertEqual("current_timestamp", CurrentTimestamp())
       assertEqual("current_time", CurrentTime())
+      assertEqual("localtime", CurrentTime())
     }
 
     def testNonAnsiBehavior(): Unit = {
       assertEqual("current_date", UnresolvedAttribute.quoted("current_date"))
       assertEqual("current_timestamp", 
UnresolvedAttribute.quoted("current_timestamp"))
       assertEqual("current_time", UnresolvedAttribute.quoted("current_time"))
+      assertEqual("localtime", UnresolvedAttribute.quoted("localtime"))
     }
     withSQLConf(
       SQLConf.ANSI_ENABLED.key -> "false",
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 6297aece4cbb..d8abc643afeb 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
@@ -110,6 +110,7 @@
 | org.apache.spark.sql.catalyst.expressions.CurrentDate | current_date | 
SELECT current_date() | struct<current_date():date> |
 | org.apache.spark.sql.catalyst.expressions.CurrentPath | current_path | 
SELECT current_path() | struct<current_path():string> |
 | org.apache.spark.sql.catalyst.expressions.CurrentTime | current_time | 
SELECT current_time() | struct<current_time(6):time(6)> |
+| org.apache.spark.sql.catalyst.expressions.CurrentTime | localtime | SELECT 
localtime() | struct<current_time(6):time(6)> |
 | org.apache.spark.sql.catalyst.expressions.CurrentTimeZone | current_timezone 
| SELECT current_timezone() | struct<current_timezone():string> |
 | org.apache.spark.sql.catalyst.expressions.CurrentTimestamp | 
current_timestamp | SELECT current_timestamp() | 
struct<current_timestamp():timestamp> |
 | org.apache.spark.sql.catalyst.expressions.CurrentUser | current_user | 
SELECT current_user() | struct<current_user():string> |
diff --git 
a/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out 
b/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
index 8482a10b2292..cbee1375aba8 100644
--- a/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
@@ -221,6 +221,7 @@ LINES       false
 LIST   false
 LOAD   false
 LOCAL  false
+LOCALTIME      true
 LOCATION       false
 LOCK   false
 LOCKS  false
@@ -489,6 +490,7 @@ JOIN
 LATERAL
 LEADING
 LEFT
+LOCALTIME
 NATURAL
 NOT
 NULL
diff --git a/sql/core/src/test/resources/sql-tests/results/keywords.sql.out 
b/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
index 4a48c1c01d7b..dfdbc5fccb56 100644
--- a/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
@@ -221,6 +221,7 @@ LINES       false
 LIST   false
 LOAD   false
 LOCAL  false
+LOCALTIME      false
 LOCATION       false
 LOCK   false
 LOCKS  false
diff --git 
a/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out 
b/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
index 4a48c1c01d7b..dfdbc5fccb56 100644
--- a/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
@@ -221,6 +221,7 @@ LINES       false
 LIST   false
 LOAD   false
 LOCAL  false
+LOCALTIME      false
 LOCATION       false
 LOCK   false
 LOCKS  false
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 29395eba2dc1..0766c9fcc3c4 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
@@ -108,6 +108,27 @@ abstract class TimeFunctionsSuiteBase extends 
SharedSparkSession {
     }
   }
 
+  test("SPARK-57558: localtime function") {
+    val df = spark.range(1)
+
+    // localtime() returns TIME with default precision
+    val result1 = df.selectExpr("localtime()")
+    assert(result1.schema.fields.head.dataType == 
TimeType(TimeType.MICROS_PRECISION))
+
+    // localtime(3) returns TIME(3)
+    val result2 = df.selectExpr("localtime(3)")
+    assert(result2.schema.fields.head.dataType == TimeType(3))
+
+    // Bare LOCALTIME keyword (no parens) returns TIME
+    val result3 = spark.sql("SELECT LOCALTIME")
+    assert(result3.schema.fields.head.dataType == 
TimeType(TimeType.MICROS_PRECISION))
+
+    // localtime() and current_time() produce approximately the same result
+    val ltResult = df.selectExpr("localtime()")
+    val ctResult = df.selectExpr("current_time()")
+    assertTwoTimesAreApproximatelyEqual(ltResult, ctResult)
+  }
+
   test("SPARK-52881: make_time function") {
     // Input data for the function.
     val schema = StructType(Seq(
diff --git 
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
index e20beaa15181..276a266f5d16 100644
--- 
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
+++ 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
@@ -214,7 +214,7 @@ trait ThriftServerWithSparkContextSuite extends 
SharedThriftServer {
       val sessionHandle = client.openSession(user, "")
       val infoValue = client.getInfo(sessionHandle, 
GetInfoType.CLI_ODBC_KEYWORDS)
       // scalastyle:off line.size.limit
-      assert(infoValue.getStringValue == 
"ADD,AFTER,AGGREGATE,ALIGN,ALL,ALTER,ALWAYS,ANALYZE,AND,ANTI,ANY,ANY_VALUE,APPROX,ARCHIVE,ARRAY,AS,ASC,ASENSITIVE,AT,ATOMIC,AUTHORIZATION,BEGIN,BERNOULLI,BETWEEN,BIGINT,BIN,BINARY,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BOOLEAN,BOTH,BUCKET,BUCKETS,BY,BYTE,CACHE,CALL,CALLED,CASCADE,CASE,CAST,CATALOG,CATALOGS,CHANGE,CHANGES,CHAR,CHARACTER,CHECK,CLEAR,CLOSE,CLUSTER,CLUSTERED,CODEGEN,COLLATE,COLLATION,COLLATIONS,COLLECTION,COLUMN,COLUMNS,COMMEN
 [...]
+      assert(infoValue.getStringValue == 
"ADD,AFTER,AGGREGATE,ALIGN,ALL,ALTER,ALWAYS,ANALYZE,AND,ANTI,ANY,ANY_VALUE,APPROX,ARCHIVE,ARRAY,AS,ASC,ASENSITIVE,AT,ATOMIC,AUTHORIZATION,BEGIN,BERNOULLI,BETWEEN,BIGINT,BIN,BINARY,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BOOLEAN,BOTH,BUCKET,BUCKETS,BY,BYTE,CACHE,CALL,CALLED,CASCADE,CASE,CAST,CATALOG,CATALOGS,CHANGE,CHANGES,CHAR,CHARACTER,CHECK,CLEAR,CLOSE,CLUSTER,CLUSTERED,CODEGEN,COLLATE,COLLATION,COLLATIONS,COLLECTION,COLUMN,COLUMNS,COMMEN
 [...]
       // scalastyle:on line.size.limit
     }
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to