[SPARK-57462][PYTHON][SQL] Add PySpark support for nanosecond-precision timestamp types - #58418
Open
stevomitric wants to merge 3 commits into
Open
[SPARK-57462][PYTHON][SQL] Add PySpark support for nanosecond-precision timestamp types#58418stevomitric wants to merge 3 commits into
stevomitric wants to merge 3 commits into
Conversation
stevomitric
force-pushed
the
stevomitric/spark-57462-pyspark-nanos
branch
4 times, most recently
from
August 31, 2026 13:02
bba625a to
b8598c7
Compare
…on timestamp types ### What changes were proposed in this pull request? Exposes the nanosecond-capable timestamp types `TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)` (`p` in [7, 9]) in PySpark. Before this change `python/pyspark/sql/types.py` defined only the microsecond singletons, so any DataFrame whose schema contained one of these types failed on the Python side even though the JVM and the Spark Connect protocol already supported them. - `python/pyspark/sql/types.py`: adds `TimestampNTZNanosType(precision)` and `TimestampLTZNanosType(precision)` (plus the shared `AnyTimestampNanoType` base, which is not exported), with precision validation, `simpleString` / `jsonValue` / `__repr__`, and `toInternal` / `fromInternal`. Registers the parameterized JSON type names, mirroring `DataType.parseDataType` in `sql/api`: precision 6 maps to the standard microsecond type, 7-9 to the nanosecond types, and any other precision is rejected. Also registers the two types in `_acceptable_types` (used by `_make_type_verifier`, so `createDataFrame` accepts `datetime.datetime` values) and in `_get_jvm_type_name` (so `printSchema()` renders `timestamp_ntz(9)` rather than a name derived from the class). - `python/pyspark/sql/connect/types.py`: converts the two types to and from the Connect `DataType` proto in both directions, treating an omitted `precision` as 9 per types.proto. - `sql/api/.../types/ops/TimestampNanosTypeApiOps.scala`: implements the Types Framework Python-interop hooks (`needConversionInPython`, `makeFromJava`) so values round-trip over Py4J. Without these, `EvaluatePython.makeFromJava` fell through to its catch-all and silently produced NULL for every nanosecond column. - `sql/core/.../EvaluatePython.scala`: adds the reverse direction, converting the internal `TimestampNanosVal` to epoch microseconds. Without it the value reached the pickler as a raw `TimestampNanosVal`, which has no registered pickler. - `python/pyspark/errors/error-conditions.json`: adds `INVALID_TIMESTAMP_PRECISION`, worded to match the JVM error condition of the same name. The external Python value is `datetime.datetime`, which is microsecond-resolution, so the Py4J protocol carries epoch microseconds and sub-microsecond digits are truncated at the Python boundary in both directions. This mirrors the shipped `TimeType` behaviour and is the microsecond-only Python/UDF limitation already documented by SPARK-57808; the stored value keeps full precision. Type inference is unchanged: a bare `datetime.datetime` still infers microsecond `TimestampType`, and the nanosecond types are reachable only through an explicit schema. Arrow and pandas value conversion (`toPandas`, `createDataFrame` from pandas, and therefore the Spark Connect data path) is deliberately not included here and remains follow-up work; `to_arrow_type` continues to reject these types. ### Why are the changes needed? Without Python type classes, a `TIMESTAMP(9)` column cannot be read or written from PySpark at all: `proto_schema_to_pyspark_data_type` raised `UNSUPPORTED_OPERATION` for the Connect schema, and reading `df.schema` failed because the parameterized JSON type name had no Python parser. This is the last missing client for the umbrella SPARK-56822. ### Does this PR introduce _any_ user-facing change? Yes. `TimestampNTZNanosType` and `TimestampLTZNanosType` are new public types in `pyspark.sql.types`. They are only reachable via an explicit schema or a nanosecond-typed query result, and the server keeps them behind the `spark.sql.timestampNanosTypes.enabled` preview flag, so no existing behaviour changes. ### How was this patch tested? New tests in `python/pyspark/sql/tests/test_types.py`: - `DataTypeTests`: precision validation (7-9 accepted; -1/0/5/6/10 rejected with `INVALID_TIMESTAMP_PRECISION`), string representations including `printSchema()` rendering, equality / hashing / pickling, JSON parsing across precisions including the 6 -> microsecond mapping and the rejected precisions, nested array/map/struct JSON round-trip, and `toInternal` / `fromInternal` agreement with the microsecond types. - `DataTypeVerificationTests`: accepted and rejected values for both types. - `TypesTestsMixin.test_timestamp_nanos_type`: DDL parse agreement with the JVM, plus a `createDataFrame` / `collect` round-trip with nulls, and a check that a value stored at nanosecond precision still renders 9 fractional digits server-side while truncating to microseconds when collected as a `datetime`. New test in `python/pyspark/sql/tests/connect/test_connect_plan.py`: DataType proto round-trip for both types across precisions, nested, and with `precision` omitted. Co-authored-by: Isaac <no-reply@databricks.com>
stevomitric
force-pushed
the
stevomitric/spark-57462-pyspark-nanos
branch
from
August 31, 2026 15:55
b8598c7 to
f8b74b1
Compare
uros-b
reviewed
Sep 1, 2026
… nanosecond timestamp types ### What changes were proposed in this pull request? Adds two tests to `python/pyspark/sql/tests/test_types.py` that drive a nanosecond timestamp value *into* a classic Python UDF as an argument, exercising the JVM -> Python direction of `EvaluatePython.toJava` through the UDF-input caller: - `test_timestamp_nanos_type_python_udf_input`: `udf(lambda x: x, TimestampNTZNanosType(9))` over a nanosecond column, covering the `TimestampNanosVal -> epochMicros` scalar arm. - `test_timestamp_nanos_type_map_key_python_udf_input`: a map with nanosecond keys fed into a UDF, reaching the map-key rejection branch (`TIMESTAMP_NANOS_PYTHON_MAP_KEY`) in `EvaluatePython.toJava`. The UDF returns a non-map type, so the result collect does not re-trip the earlier Python-side guard in `classic/dataframe.py`. ### Why are the changes needed? Addresses review feedback: the existing nanosecond UDF test only *returns* a nanosecond value, and the map-key collision test uses `collect()`, which trips the Python-side guard in `classic/dataframe.py` before reaching the JVM `toJava` throw. The `toJava` scalar arm reached via UDF input, and the map-key throw branch (whose own comment cites the Python-UDF input path), were therefore unexercised. ### Does this PR introduce _any_ user-facing change? No. Test-only. ### How was this patch tested? New session tests in `python/pyspark/sql/tests/test_types.py`, run under CI (both require a live SparkSession). `useArrow=False` forces the classic Py4J path. Co-authored-by: Isaac <no-reply@databricks.com>
stevomitric
force-pushed
the
stevomitric/spark-57462-pyspark-nanos
branch
from
September 3, 2026 09:14
420f06f to
7e3668c
Compare
uros-b
reviewed
Sep 3, 2026
| Super class of the nanosecond-capable timestamp data types | ||
| :class:`TimestampNTZNanosType` and :class:`TimestampLTZNanosType`. | ||
|
|
||
| .. versionadded:: 5.0.0 |
Member
There was a problem hiding this comment.
Suggested change
| .. versionadded:: 5.0.0 | |
| .. versionadded:: 4.4.0 |
uros-b
reviewed
Sep 3, 2026
| Spark Connect data path -- is not yet supported and raises | ||
| ``UNSUPPORTED_DATA_TYPE_FOR_ARROW_CONVERSION``; it is planned as a follow-up. | ||
|
|
||
| .. versionadded:: 5.0.0 |
Member
There was a problem hiding this comment.
Suggested change
| .. versionadded:: 5.0.0 | |
| .. versionadded:: 4.4.0 |
uros-b
reviewed
Sep 3, 2026
| Carries the same microsecond-only Python boundary as :class:`TimestampNTZNanosType`; see the | ||
| notes there. | ||
|
|
||
| .. versionadded:: 5.0.0 |
Member
There was a problem hiding this comment.
Suggested change
| .. versionadded:: 5.0.0 | |
| .. versionadded:: 4.4.0 |
uros-b
reviewed
Sep 3, 2026
| def count(self) -> int: | ||
| return int(self._jdf.count()) | ||
|
|
||
| def _check_timestamp_nanos_map_key(self) -> None: |
Member
There was a problem hiding this comment.
The Python-side TIMESTAMP_NANOS_PYTHON_MAP_KEY fills {"type": self.schema.simpleString()} (the whole DataFrame schema) while the message reads "Cannot convert a map with <type> keys...", so it renders e.g. "a map with struct<m:map<timestamp_ntz(9),int>> keys"; the JVM twin (EvaluatePython.toJava) passes just mt.keyType.sql. Pass the offending key type (or reword) for an accurate, consistent message.
…timestamp types Addresses review feedback: 4.4.0 is the next feature release (branch-4.x is at 4.4.0.dev0) and matches the other recent versionadded:: 4.4.0 entries. master's version.py (5.0.0.dev0) is a longer-horizon placeholder, not the release this ships in. Co-authored-by: Isaac <no-reply@databricks.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
Exposes the nanosecond-capable timestamp types
TIMESTAMP_NTZ(p)/TIMESTAMP_LTZ(p)(pin [7, 9]) in PySpark. Before this changepython/pyspark/sql/types.pydefined only the microsecond singletons, so any DataFrame whose schema contained one of these types failed on the Python side even though the JVM and the Spark Connect protocol already supported them.python/pyspark/sql/types.py: addsTimestampNTZNanosType(precision)andTimestampLTZNanosType(precision)(plus the sharedAnyTimestampNanoTypebase, which is not exported), with precision validation,simpleString/jsonValue/__repr__, andtoInternal/fromInternal. Registers the parameterized JSON type names, mirroringDataType.parseDataTypeinsql/api: precision 6 maps to the standard microsecond type, 7-9 to the nanosecond types, and any other precision is rejected. Also registers the two types in_acceptable_types(used by_make_type_verifier, socreateDataFrameacceptsdatetime.datetimevalues) and in_get_jvm_type_name(soprintSchema()renderstimestamp_ntz(9)rather than a name derived from the class).python/pyspark/sql/connect/types.py: converts the two types to and from the ConnectDataTypeproto in both directions, treating an omittedprecisionas 9 per types.proto.sql/api/.../types/ops/TimestampNanosTypeApiOps.scala: implements the Types Framework Python-interop hooks (needConversionInPython,makeFromJava) so values round-trip over Py4J. Without these,EvaluatePython.makeFromJavafell through to its catch-all and silently produced NULL for every nanosecond column.sql/core/.../EvaluatePython.scala: adds the reverse direction, converting the internalTimestampNanosValto epoch microseconds. Without it the value reached the pickler as a rawTimestampNanosVal, which has no registered pickler.python/pyspark/errors/error-conditions.json: addsINVALID_TIMESTAMP_PRECISION, worded to match the JVM error condition of the same name.The external Python value is
datetime.datetime, which is microsecond-resolution, so the Py4J protocol carries epoch microseconds and sub-microsecond digits are truncated at the Python boundary in both directions. This mirrors the shippedTimeTypebehaviour and is the microsecond-only Python/UDF limitation already documented by SPARK-57808; the stored value keeps full precision. Type inference is unchanged: a baredatetime.datetimestill infers microsecondTimestampType, and the nanosecond types are reachable only through an explicit schema.Arrow and pandas value conversion (
toPandas,createDataFramefrom pandas, and therefore the Spark Connect data path) is deliberately not included here and remains follow-up work;to_arrow_typecontinues to reject these types.Why are the changes needed?
Without Python type classes, a
TIMESTAMP(9)column cannot be read or written from PySpark at all:proto_schema_to_pyspark_data_typeraisedUNSUPPORTED_OPERATIONfor the Connect schema, and readingdf.schemafailed because the parameterized JSON type name had no Python parser. This is the last missing client for the umbrella SPARK-56822.Does this PR introduce any user-facing change?
Yes.
TimestampNTZNanosTypeandTimestampLTZNanosTypeare new public types inpyspark.sql.types. They are only reachable via an explicit schema or a nanosecond-typed query result, and the server keeps them behind thespark.sql.timestampNanosTypes.enabledpreview flag, so no existing behaviour changes.How was this patch tested?
New tests in
python/pyspark/sql/tests/test_types.py:DataTypeTests: precision validation (7-9 accepted; -1/0/5/6/10 rejected withINVALID_TIMESTAMP_PRECISION), string representations includingprintSchema()rendering, equality / hashing / pickling, JSON parsing across precisions including the 6 -> microsecond mapping and the rejected precisions, nested array/map/struct JSON round-trip, andtoInternal/fromInternalagreement with the microsecond types.DataTypeVerificationTests: accepted and rejected values for both types.TypesTestsMixin.test_timestamp_nanos_type: DDL parse agreement with the JVM, plus acreateDataFrame/collectround-trip with nulls, and a check that a value stored at nanosecond precision still renders 9 fractional digits server-side while truncating to microseconds when collected as adatetime.New test in
python/pyspark/sql/tests/connect/test_connect_plan.py: DataType proto round-trip for both types across precisions, nested, and withprecisionomitted.Was this patch authored or co-authored using generative AI tooling?
Co-authored: Claude Opus 5