You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/09/03 15:27:58 UTC
(spark-connect-swift) branch main updated: [SPARK-59203] Support nanosecond timestamp literals in `lit` and SQL parameter binding
This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-connect-swift.git
The following commit(s) were added to refs/heads/main by this push:
new 9ae3226 [SPARK-59203] Support nanosecond timestamp literals in `lit` and SQL parameter binding
9ae3226 is described below
commit 9ae3226a81160fa6da530390041e23c2d0041ce3
Author: Dongjoon Hyun <do...@apache.org>
AuthorDate: Thu Sep 3 08:27:50 2026 -0700
[SPARK-59203] Support nanosecond timestamp literals in `lit` and SQL parameter binding
### What changes were proposed in this pull request?
This PR aims to support nanosecond timestamp literals in `lit` and SQL parameter binding.
- Add `lit(_ value: TimestampNanos)` which builds `Expression.Literal.TimestampLTZNanos` with `epochMicros`, `nanosWithinMicro`, and `precision = 9`.
- Conform `TimestampNanos` to `SparkLiteral` so it can be used directly as an operand of `Column` operators, e.g. `col("t") == ts`.
- Handle `TimestampNanos` in `ExpressionLiteral.init(_:)` so it can be bound as a positional (`?`) or named (`:name`) parameter of `spark.sql`.
Like `Date`, which maps to `TIMESTAMP` (`TIMESTAMP_LTZ`), `TimestampNanos` is an absolute point in time and maps to `TIMESTAMP_LTZ(9)`.
```swift
try await spark.conf.set("spark.sql.timestampNanosTypes.enabled", "true")
let ts = TimestampNanos(epochNanos: 1_767_225_600_123_456_789)
spark.range(1).select(lit(ts)) // timestamp_ltz(9)
try await spark.sql("SELECT ?", ts)
try await spark.sql("SELECT :t", args: ["t": ts])
```
### Why are the changes needed?
SPARK-59198 covered only the read path (`DataFrame.collect`). This completes the write path for expressions so that Swift users can send nanosecond timestamp values to the server via `lit`, `Column` operators, and parameterized SQL without losing sub-microsecond digits.
### Does this PR introduce _any_ user-facing change?
Yes, `TimestampNanos` is newly accepted by `lit`, `Column` operators, and `spark.sql` parameters. There is no behavior change for existing code.
### How was this patch tested?
Pass the CIs with newly added test cases.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Fable 5.1
Closes #512 from dongjoon-hyun/SPARK-59203.
Authored-by: Dongjoon Hyun <do...@apache.org>
Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
Sources/SparkConnect/Extension.swift | 6 ++++++
Sources/SparkConnect/Functions.swift | 15 +++++++++++++++
Tests/SparkConnectTests/DataFrameTests.swift | 18 ++++++++++++++++++
Tests/SparkConnectTests/FunctionsTests.swift | 8 ++++++++
Tests/SparkConnectTests/SparkSessionTests.swift | 16 ++++++++++++++++
5 files changed, 63 insertions(+)
diff --git a/Sources/SparkConnect/Extension.swift b/Sources/SparkConnect/Extension.swift
index c33f07d..dc0575f 100644
--- a/Sources/SparkConnect/Extension.swift
+++ b/Sources/SparkConnect/Extension.swift
@@ -200,6 +200,12 @@ extension ExpressionLiteral {
case let value as LocalTime:
self.time.nano = value.nanoOfDay
self.time.precision = 6
+ case let value as TimestampNanos:
+ // Like `Date`, `TimestampNanos` is an absolute point in time, so it maps to
+ // Spark's `TIMESTAMP_LTZ(9)`.
+ self.timestampLtzNanos.epochMicros = value.epochMicros
+ self.timestampLtzNanos.nanosWithinMicro = Int32(value.nanosWithinMicro)
+ self.timestampLtzNanos.precision = 9
default:
if case Optional<Any>.none = value as Any {
var dataType = ProtoDataType()
diff --git a/Sources/SparkConnect/Functions.swift b/Sources/SparkConnect/Functions.swift
index d9c5631..dac4b9c 100644
--- a/Sources/SparkConnect/Functions.swift
+++ b/Sources/SparkConnect/Functions.swift
@@ -122,6 +122,17 @@ public func lit(_ value: LocalTime) -> Column {
return litColumn(literal)
}
+/// Creates a ``Column`` of `TIMESTAMP_LTZ(9)` literal value.
+/// - Parameter value: A literal value.
+/// - Returns: A ``Column``.
+public func lit(_ value: TimestampNanos) -> Column {
+ var literal = ExpressionLiteral()
+ literal.timestampLtzNanos.epochMicros = value.epochMicros
+ literal.timestampLtzNanos.nanosWithinMicro = Int32(value.nanosWithinMicro)
+ literal.timestampLtzNanos.precision = 9
+ return litColumn(literal)
+}
+
/// A type that can be used as a literal operand of ``Column`` operators.
///
/// This allows Swift literals to be mixed with ``Column`` expressions directly
@@ -175,6 +186,10 @@ extension LocalTime: SparkLiteral {
public var toLiteralColumn: Column { lit(self) }
}
+extension TimestampNanos: SparkLiteral {
+ public var toLiteralColumn: Column { lit(self) }
+}
+
/// Returns a sort expression based on the ascending order of the given column name.
/// - Parameter name: A column name.
/// - Returns: A ``Column`` with ascending sort order.
diff --git a/Tests/SparkConnectTests/DataFrameTests.swift b/Tests/SparkConnectTests/DataFrameTests.swift
index 52a39a8..c9511b0 100644
--- a/Tests/SparkConnectTests/DataFrameTests.swift
+++ b/Tests/SparkConnectTests/DataFrameTests.swift
@@ -233,6 +233,24 @@ struct DataFrameTests {
await spark.stop()
}
+ @Test
+ func selectTimestampNanosLiteral() async throws {
+ let spark = try await SparkSession.builder.getOrCreate()
+ if await isSparkVersionAtLeast(spark.version, "4.3") {
+ try await spark.conf.set("spark.sql.timestampNanosTypes.enabled", "true")
+ for ts in [
+ TimestampNanos(epochNanos: 1_767_225_600_123_456_789),
+ TimestampNanos(epochNanos: 0),
+ TimestampNanos(epochNanos: -1),
+ ] {
+ let df = try await spark.range(1).select(lit(ts))
+ #expect(try await df.dtypes[0].1 == "timestamp_ltz(9)")
+ #expect(try await df.collect() == [Row(ts)])
+ }
+ }
+ await spark.stop()
+ }
+
@Test
func selectTimeLiteral() async throws {
let spark = try await SparkSession.builder.getOrCreate()
diff --git a/Tests/SparkConnectTests/FunctionsTests.swift b/Tests/SparkConnectTests/FunctionsTests.swift
index 92df869..7087437 100644
--- a/Tests/SparkConnectTests/FunctionsTests.swift
+++ b/Tests/SparkConnectTests/FunctionsTests.swift
@@ -60,6 +60,10 @@ struct FunctionsTests {
let time = try #require(LocalTime(hour: 12, minute: 34, second: 56))
#expect(lit(time).expr.literal.time.nano == 45_296_000_000_000)
#expect(lit(time).expr.literal.time.precision == 6)
+ let ts = TimestampNanos(epochNanos: 1_767_225_600_123_456_789)
+ #expect(lit(ts).expr.literal.timestampLtzNanos.epochMicros == 1_767_225_600_123_456)
+ #expect(lit(ts).expr.literal.timestampLtzNanos.nanosWithinMicro == 789)
+ #expect(lit(ts).expr.literal.timestampLtzNanos.precision == 9)
}
@Test
@@ -260,6 +264,10 @@ struct FunctionsTests {
#expect(
(col("a") == time).expr.unresolvedFunction.arguments[1].literal.time.nano
== 45_296_000_000_000)
+ let ts = TimestampNanos(epochNanos: 1_767_225_600_123_456_789)
+ #expect(
+ (col("a") == ts).expr.unresolvedFunction.arguments[1].literal.timestampLtzNanos.epochMicros
+ == 1_767_225_600_123_456)
}
@Test
diff --git a/Tests/SparkConnectTests/SparkSessionTests.swift b/Tests/SparkConnectTests/SparkSessionTests.swift
index 0d92e9b..2d17096 100644
--- a/Tests/SparkConnectTests/SparkSessionTests.swift
+++ b/Tests/SparkConnectTests/SparkSessionTests.swift
@@ -305,6 +305,22 @@ struct SparkSessionTests {
await spark.stop()
}
+ @Test
+ func sqlWithTimestampNanosParameter() async throws {
+ await SparkSession.builder.clear()
+ let spark = try await SparkSession.builder.getOrCreate()
+ if await isSparkVersionAtLeast(spark.version, "4.3") {
+ try await spark.conf.set("spark.sql.timestampNanosTypes.enabled", "true")
+ let ts = TimestampNanos(epochNanos: 1_767_225_600_123_456_789)
+ #expect(try await spark.sql("SELECT typeof(?)", ts).collect() == [Row("timestamp_ltz(9)")])
+ #expect(try await spark.sql("SELECT ?", ts).collect() == [Row(ts)])
+ #expect(try await spark.sql("SELECT :t", args: ["t": ts]).collect() == [Row(ts)])
+ let before1970 = TimestampNanos(epochNanos: -1)
+ #expect(try await spark.sql("SELECT :t", args: ["t": before1970]).collect() == [Row(before1970)])
+ }
+ await spark.stop()
+ }
+
@Test
func sqlWithUnsupportedParameterType() async throws {
await SparkSession.builder.clear()
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org