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]