You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@spark.apache.org by "Max Gekk (Jira)" <ji...@apache.org> on 2026/05/11 15:17:00 UTC

[jira] [Updated] (SPARK-56822) SPIP: Timestamps with nanosecond precision

     [ https://issues.apache.org/jira/browse/SPARK-56822?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Max Gekk updated SPARK-56822:
-----------------------------
    Description: 
h1. Q1. What are you trying to do? Articulate your objectives using absolutely no jargon.

 

Add nanosecond-capable timestamps with an explicit fractional precision *n* in SQL and APIs, while keeping a simple binary value: epoch microseconds (64-bit) + nanoseconds within that microsecond (0–999, stored in 16 bits).
h3. SQL surface

Support TIMESTAMP_NTZ(n), TIMESTAMP_LTZ(n), and TIMESTAMP(n) in the parser (including equivalent spellings: WITHOUT TIME ZONE, WITH LOCAL TIME ZONE, etc.) so precision is first-class in the grammar, not a side convention. The parameter n is optional, and its valid range is [0, 9]. It defines how many decimal digits of the fractional second are part of the type. For example: n = 6 -> microseconds, n = 9 -> nanoseconds.
h3. Scala / Java APIs

Introduce parameterized catalyst types, e.g. TimestampNTZNanosType(n) for TIMESTAMP_NTZ(n) and TimestampNanosType(n) for TIMESTAMP_LTZ(n).
h3. In-memory value

The internal representation of both types is long micros since the epoch + short (or 16-bit) nanos-in-micro in [0, 999]. Both NTZ and LTZ have the same representation; time zone only affects interpretation for LTZ, not the pair layout.

 
h1. Q2. What problem is this proposal NOT designed to solve?
 * *Precision below nanosecond-target range.*

This SPIP only covers high-precision parameterized timestamps where 7 <= n <= 9. Support/changes for n < 7 are explicitly out of scope for this proposal.
 * *Subtraction of timestamps.* The result should be a day-time interval but the data type has microsecond precision at the moment.

 * *Changing existing time zone semantics.*

It does not redefine NTZ/LTZ semantics, session time zone behavior, or SQL time zone rules.
 * *A full rework of all connectors/storage formats*

It does not promise end-to-end nanosecond support in every external system; connector-specific follow-ups may still be needed.

 
h1. Q3. How is it done today, and what are the limits of current practice?

Today Spark SQL supports two built-in timestamp types, both at microsecond precision:
 * TimestampType (TIMESTAMP WITH LOCAL TIME ZONE)
 * TimestampNTZType (TIMESTAMP WITHOUT TIME ZONE)

So Spark’s native timestamp model stops at 6 fractional digits.

 

For nanosecond data (for example Parquet TIMESTAMP(NANOS, ...)), current behavior is limited:
 * Default behavior: Spark rejects it with an analysis error (for example: Illegal Parquet type: INT64 (TIMESTAMP(NANOS,true))).
 * Legacy fallback (spark.sql.legacy.parquet.nanosAsLong=true): Spark reads it as LongType (raw integer), which drops timestamp semantics:
 * no timestamp/date function behavior
 * no time zone semantics
 * no timestamp type safety

In practice this means users either:
 * down-convert data to microseconds before Spark, or
 * keep nanosecond values as raw integers and do manual conversion logic, or
 * switch to another engine for nanosecond timestamp workloads.

 

This is increasingly painful because nanosecond timestamps are common in data produced by systems like Pandas/PyArrow, Trino, ClickHouse, and DuckDB, and in domains such as market data, IoT telemetry, and tracing/observability pipelines.

 
h1. Q4. What is new in your approach and why do you think it will be successful?
h2. What is new
 # {*}Precision parameterization for timestamp types{*}
Add TIMESTAMP_NTZ(n), TIMESTAMP_LTZ(n), and TIMESTAMP(n) (including alias forms) with explicit fractional precision, focused on 7 <= n <= 9.
 # {*}New catalyst/API types for nanos-capable timestamps{*}
Introduce parameterized nanos-aware types instead of overloading existing microsecond-only types.
 * For TIMESTAMP_NTZ(n):

case class TimestampNTZNanosType(precision: Int)
 * For TIMESTAMP_LTZ(n):

case class TimestampLTZNanosType(precision: Int)
 # {*}Precise internal value model without changing core epoch semantics{*}
Represent each value as:
 * epoch microseconds (long)
 * nanoseconds within the microsecond (0..999) This preserves existing Spark timestamp foundations while adding sub-micro precision.


 # {*}End-to-end engine integration, not just parser support{*}
The change is wired through parser/type system, expression evaluation, codegen, unsafe/container paths, and file-format handling paths needed for practical use.

h2. Why this should succeed
 # Low semantic risk: extends existing timestamp families instead of redefining them.
 # Backward compatible path: existing microsecond types and behavior remain unchanged.
 # Incremental implementation: narrow scope (7..9) and clear boundaries make rollout testable.
 # Real interoperability value: directly addresses common nanos data sources where Spark currently fails or degrades to LongType.
 # Operationally practical: keeps compact representation and reuses current execution architecture, so performance/regression risk is manageable.

 
h1. Q5. Who cares? If you are successful, what difference will it make?
h2. Who cares
 * Data engineering teams ingesting Parquet/warehouse data produced with nanosecond timestamps.
 * Platform teams running mixed ecosystems (Spark + Trino/ClickHouse/DuckDB/Pandas/PyArrow).
 * Domain teams where sub-micro timing matters (market data, telemetry, observability, IoT, CDC/event streams).
 * Connector and table-format maintainers who currently need compatibility workarounds.

h2. What difference success makes
 # Interop works by default. Spark can read/use high-precision timestamp data as timestamp types, instead of failing or forcing LongType fallback.
 # No more semantic loss workarounds. Users avoid manual bigint-to-timestamp conversions, custom UDF glue, and loss of timezone/type semantics.
 # Correctness for high-frequency data. Distinct events within the same microsecond stay distinct; ordering and time-window logic become more reliable for nanos data.
 # Lower migration friction to Spark. Teams can bring existing nanos datasets into Spark pipelines without pre-normalization to micros.
 # Cleaner long-term type story. Spark gets an explicit precision model for timestamps (n) rather than implicit microsecond-only behavior, making schema contracts clearer across SQL and APIs.

 
h1. Q6. What are the risks?
|Risk|Mitigation|
|*User confusion* from more timestamp spellings (TIMESTAMP(n), TIMESTAMP_NTZ(n), TIMESTAMP_LTZ(n), plus WITHOUT TIME ZONE / WITH LOCAL TIME ZONE variants).|Keep today’s types as the default; require explicit n for the new behavior; document a small “cheat sheet” mapping SQL spelling -> semantics + precision.|
|{*}Wrong results in shared datetime code paths{*}: functions/rules that implicitly assume microsecond-only timestamps (AnyTimestampType-style plumbing, codegen, optimizer rewrites) may mishandle nanosecond-capable values.|Systematic audit of shared abstractions (casts, comparisons, intervals, extract, codegen paths) plus focused regression tests on n ∈ [7,9].|
|*Performance regressions* (wider projections, more branches in codegen/vectorized paths).|Benchmark common scans/joins/aggregations; keep fast paths for existing microsecond timestamps.|
|*Range / overflow issues tied to widening.* Bugs where code accidentally converts to a single epochNanos long during promotion or builtins.|Document the representable range for the chosen internal representation; enforce bounds at cast boundaries; tests for edge instants.|
|*Interop / external formats (Parquet/Iceberg/etc.):* external encodings may use epoch nanoseconds in int64, while Spark uses (micros, nanosWithinMicro). Conversion bugs are likely near boundaries and for pushdown predicates.|Conversions must be explicitly specified (including acceptable rounding/truncation) and covered by tests for the supported read/write paths you ship.|

 
h1. Q7. How long will it take?

This estimate covers shipping of parameterized nanosecond-capable timestamps (7 <= n <= 9) with feature parity to existing TimestampType / TimestampNTZType, including parser, core type system, Parquet nanos support, encoders/converters, datetime utilities, cast matrix (interpreted + codegen), expression updates, type coercion, literals, and testing.


Implementation should integrate with the Types Framework (SPARK-53504): register the new types through the centralized TypeOps / TypeApiOps (and storage/client Ops as applicable) instead of scattering one-off integration across dozens of files.

 
h3. Engineering estimate (person-weeks)
|Area|pw|
|Core type system (parameterization, physical dispatch for p<=6 vs p>=7, equality/hash/order + catalyst plumbing)|2|
|SQL parser / AST (TIMESTAMP…(p) + aligned spellings)|1|
|Parquet nanos read/write (incl. vectorized + non-vectorized, rebasing, legacy migration hooks, preview flags)|4|
|Encoders / converters (framework-aligned)|1.5|
|Datetime utils (nano parsing/conversion/arithmetic helpers)|1|
|Cast matrix (interpreted + codegen, precision rules)|2|
|Expression parity work (datetime builtins impacted)|5|
|Type coercion / widening (TypeCoercion / AnsiTypeCoercion)|3|
|Literals|1|
|Testing (unit + Parquet + overflow/range + ANSI vs non-ANSI)|2|
|Types Framework wiring + tests for the new type|1.5|
|Total|25.0|

 

Calendar time (rule of thumb) ~25 person-weeks is about:
 * ~6 months for one senior engineer (calendar), or
 * ~7–9 weeks wall time for two engineers mostly dedicated,

plus ~2–4 extra weeks calendar buffer for OSS review/CI churn and rebases.

 
h1. Q8. What are the mid-term and final “exams” to check for success?
h3. Mid-term “exams”. Users can declare, load, and analyze nanosecond-capable timestamps without hacks:
 # {*}SQL types are real, not experimental stubs.{*}
Users can declare schemas with TIMESTAMP…(p) / TIMESTAMP_NTZ(p) / TIMESTAMP_LTZ(p) for p ∈ [7,9] (including equivalent WITHOUT TIME ZONE / WITH LOCAL TIME ZONE spellings), and those types round-trip through CREATE TABLE / CAST / DESCRIBE / explain plans in a predictable way.
 # {*}Nanosecond timestamps stop degrading into “just BIGINT”.{*}
Users can read common nanosecond Parquet inputs as timestamps, without being forced into LongType / legacy escape hatches for the supported paths you ship in this milestone.
 # {*}Everyday analytics works on the new types.{*}
For the shipped surface area, users can run typical workflows end-to-end: filters/joins/group-by keys, casts, timestamp arithmetic, extract/truncate-style operations.
 # {*}Defaults unchanged for existing users.{*}
Existing microsecond-first workloads behave as today unless users explicitly opt into p ∈ [7,9] or ingest nanosecond-native sources that trigger the new behavior.

 
h3. Final “exams”. Workflows are consistently usable, documented, and migration-ready at the level users expect from Spark timestamps today:
 # {*}Parity exam vs existing Spark timestamps (user-visible).{*}
For TimestampType / TimestampNTZType, users already expect a broad set of behaviors. For p ∈ [7,9], the shipped release must meet the same practical usability standard on the supported operations list (no “half the functions silently downgrade precision or fail inconsistently”).
 # {*}Interop exam.{*}
Users can run realistic pipelines: read nanosecond-rich Parquet, transform, and write/publish results without mandatory pre-processing outside Spark—within the explicitly documented guarantees (lossless round-trip everywhere is not required, but what is guaranteed must be testably true).
 # {*}Migration exam.{*}
There is a published migration path away from workflows that today rely on spark.sql.legacy.parquet.nanosAsLong (including what changes in schema types and what users must do).
 # {*}Documentation exam.{*}
Public docs answer: what p means, 7–9 scope, range/overflow behavior, ANSI vs non-ANSI, and known limitations—written so support/engineering can deflect repeated confusion.

 
h1. Appendix A. Proposed API Changes
h2. A.1 SQL language

Extend fractional-second precision (FSP) on timestamp families
|*Surface*|*Proposed syntax*|*Role*|
|NTZ nanos-capable|TIMESTAMP_NTZ(n)|Explicit NTZ with fractional precision {*}n{*}.|
|Alias (NTZ)|TIMESTAMP(n) WITHOUT TIME ZONE|Same NTZ meaning as TIMESTAMP_NTZ(n) (aligned with SQL-style spelling).|
|LTZ nanos-capable|TIMESTAMP_LTZ(n)|Explicit LTZ / “instant” timeline with n.|
|Alias (LTZ)|TIMESTAMP(n) WITH LOCAL TIME ZONE|Same LTZ meaning as TIMESTAMP_LTZ(n).|
|Session default timestamp form|TIMESTAMP(n)|Where Spark today resolves TIMESTAMP to LTZ or NTZ per configuration, n attaches FSP to that choice.|

Backward compatibility
 * Unparameterized forms keep today’s meaning:
 * TIMESTAMP_NTZ -> existing TimestampNTZType (microsecond semantics).
 * TIMESTAMP_LTZ / TIMESTAMP WITHOUT TIME ZONE / WITH LOCAL TIME ZONE combinations -> existing TimestampType / TimestampNTZType behavior unchanged when (n) is omitted.


 * New behavior appears only when (n) is present (and, per SPIP scope, especially 7 <= n <= 9), mapping to the new catalyst types below - not silent widening of old types.

 
h2. A.2 Scala / Java DataType APIs

New case classes:
|Type|Purpose|Notes|
|TimestampNTZNanosType(n: Int)|SQL TIMESTAMP_NTZ(n) (nanosecond-capable NTZ).|Present on this branch: sql / typeName like TIMESTAMP_NTZ(n; n in 0..9 today in code—SPIP may narrow product scope to 7..9 while keeping storage general.|
|TimestampNanosType(n: Int)|SQL TIMESTAMP_LTZ(n) (nanosecond-capable LTZ).|Proposed companion to NTZ; mirrors the same FSP parameter and external/Java mapping pattern.|

Backward compatibility
 * Existing singleton types TimestampNTZType and TimestampType remain the defaults for unparameterized SQL and for older serialized schemas that do not carry n.
 * New types are additive; code that pattern-matches only on TimestampType / TimestampNTZType continues to compile but must be reviewed for parity paths when TimestampNanosType / TimestampNTZNanosType appear.

 
h2. A.3 Compatibility summary
|Direction|Expectation|
|Old → new|Opt-in via TIMESTAMP_* (n) / new DataTypes; no change for legacy microsecond types unless users choose nanosecond-capable types.|
|New → old|Downgrades may truncate/round per FSP rules; must be documented and tested (ANSI throw vs non-ANSI null where applicable).|
|Cross-version|Schemas with nanosecond-capable types require a Spark version that understands them; older engines must reject or require migration tools—not silently coerce.|

 
h1. Appendix B. Design Sketch.

A value for nanosecond-capable NTZ (and the same pair for LTZ):

 
 * {*}epochMicros{*}: Long — signed epoch microseconds (same grid as TimestampType / TimestampNTZType today).
 * {*}nanosOfMicro{*}: Short in [0, 999] — remaining nanoseconds inside that microsecond bucket.

{*}Invariant{*}: the pair is always normalized so *nanosOfMicro* stays in range; excess carries into *epochMicros* with Math.addExact / floor-div where needed.
h4. Why this split (vs a single Long epoch-nanos counter):
 * {*}Range{*}: *epochMicros* as Long keeps the calendar reach in the same ballpark as today’s microsecond timestamps. A single INT64 epoch-nanoseconds field has a much smaller representable year range; Spark can avoid that user-visible cliff for the micros timeline part.
 * *Interop/conversion cost:* most existing Spark datetime math is already microsecond-grained; incremental changes upgrade paths by composing micro ops + cheap nano remainder, instead of forcing every operator to immediately normalize to nanoseconds-since-epoch.
 * *Deterministic normalization:* *nanosOfMicro* is a small bounded correction term - easy to audit in casts/parsers vs unconstrained nano arithmetic everywhere.

 

TimestampNTZNanosType (and similar TimestampLTZNanosType) is the schema-level description of TIMESTAMP_NTZ(p). The values for this type are not “a struct of two children in the row” at runtime; the companion internalStructType exists for metadata only (historical/compat hooks). Execution uses the normalized logical pair epoch micros + nanoseconds within that micro via org.apache.spark.unsafe.types.TimestampNTZNanos.

 

/**

 * Timestamp without time zone with fractional-second precision up to

 * nanoseconds (9 decimal digits)

 */

@Unstable

case class TimestampNTZNanosType(precision: Int) extends DatetimeType {

  if (precision < 0 || precision > 9) {

    throw DataTypeErrors.unsupportedTimestampPrecisionError(precision)

  }

 

  /**

   * Default size used by Spark for row-size estimation.

   * Nanosecond-capable NTZ values are represented logically as epoch

   * microseconds (`Long`, 8 bytes) plus nanoseconds within that micro

   * (`Short`, 2 bytes). Size estimation sums those fixed logical

   * parts (10 bytes). Physical encoding in [[org.apache.spark.sql.catalyst.expressions.UnsafeRow]]

   * uses a separate layout (one fixed pointer word plus a variable-length payload).

   */

  override def defaultSize: Int = 10

 

  override def typeName: String = s"timestamp_ntz($precision)"

  override def sql: String = s"TIMESTAMP_NTZ($precision)"

  private[spark] override def asNullable: TimestampNTZNanosType = this

}

 
h1. Appendix C. Rejected Designs

This SPIP converged on a logical pair: epoch microseconds (Long) + nanoseconds within that micro (0..999, typically Short) with a single normalization rule. Other encodings were considered and set aside for the reasons below.
h2. C.1 Epoch nanoseconds in a single Long
 * {*}Reference{*}: related direction in the broader nanosecond-timestamp SPIP / design thread ([Google Doc: SPIP: Support NanoSecond Timestamp Types|https://docs.google.com/document/d/1Q5u1whAO_KcT6d4dFFaIMy_S3RoQEo4Znwz2U-nbhls/edit?tab=t.0#heading=h.xk16mmomv6il]).
 * {*}Idea{*}: store the instant as signed 64-bit nanoseconds since Unix epoch.
 * *Why rejected* as the primary internal model for Spark’s timestamp execution:
 * Much smaller representable calendar range in INT64 nanoseconds than INT64 microseconds (a well-known “range cliff” vs today’s microsecond timestamps).
 * Higher integration cost with the existing engine, which is overwhelmingly organized around microsecond-grained datetime rules; everything would either convert constantly or risk drift.
 * Not compatible with the SQL standard

h2. C.2 Seconds + nanos of second
 * *Idea:* decompose time into whole seconds and 0..999_999_999 nanos within the second.

 * *Why rejected:*

 * Mismatches Spark’s native microsecond “currency” (long micros) used across most of Catalyst; forces pervasive rescaling and increases codegen/interpreted drift risk.
 * Second-bucket arithmetic is awkward for interval APIs and microsecond-legacy behavior (many paths think in micros, not “seconds + intra-second nanos”).

h2. C.3 Days + nanos within the day
 * {*}Idea{*}: pack calendar date as day index and time-of-day as nanoseconds within the day.

 * *Why rejected:*

 * Not aligned with how Spark’s datetime system reasons about instants (epoch micros + zone rules, legacy rebasing, Julian/Gregorian transitions, etc.).
 * Day-boundary corner cases (leap seconds notwithstandings, DST is less relevant to NTZ/LTZ storage but the model still composes poorly with “micros since epoch” execution).

h2. C.4 Nanos-from-epoch + Byte “high extension”
 * {*}Idea{*}: keep nanosecond resolution but add an extra byte to extend effective range beyond pure INT64 nanos limits.

 * *Why rejected:*

 * Non-standard, harder to explain, and complicates every operator (suddenly values are a 2-part variable-precision integer in the hot path).
 * Interoperability pain: external systems and file formats won’t natively match the “byte extension” scheme; you still convert at boundaries.

  was:TODO


> SPIP: Timestamps with nanosecond precision
> ------------------------------------------
>
>                 Key: SPARK-56822
>                 URL: https://issues.apache.org/jira/browse/SPARK-56822
>             Project: Spark
>          Issue Type: Improvement
>          Components: SQL
>    Affects Versions: 4.2.0
>            Reporter: Max Gekk
>            Assignee: Max Gekk
>            Priority: Major
>
> h1. Q1. What are you trying to do? Articulate your objectives using absolutely no jargon.
>  
> Add nanosecond-capable timestamps with an explicit fractional precision *n* in SQL and APIs, while keeping a simple binary value: epoch microseconds (64-bit) + nanoseconds within that microsecond (0–999, stored in 16 bits).
> h3. SQL surface
> Support TIMESTAMP_NTZ(n), TIMESTAMP_LTZ(n), and TIMESTAMP(n) in the parser (including equivalent spellings: WITHOUT TIME ZONE, WITH LOCAL TIME ZONE, etc.) so precision is first-class in the grammar, not a side convention. The parameter n is optional, and its valid range is [0, 9]. It defines how many decimal digits of the fractional second are part of the type. For example: n = 6 -> microseconds, n = 9 -> nanoseconds.
> h3. Scala / Java APIs
> Introduce parameterized catalyst types, e.g. TimestampNTZNanosType(n) for TIMESTAMP_NTZ(n) and TimestampNanosType(n) for TIMESTAMP_LTZ(n).
> h3. In-memory value
> The internal representation of both types is long micros since the epoch + short (or 16-bit) nanos-in-micro in [0, 999]. Both NTZ and LTZ have the same representation; time zone only affects interpretation for LTZ, not the pair layout.
>  
> h1. Q2. What problem is this proposal NOT designed to solve?
>  * *Precision below nanosecond-target range.*
> This SPIP only covers high-precision parameterized timestamps where 7 <= n <= 9. Support/changes for n < 7 are explicitly out of scope for this proposal.
>  * *Subtraction of timestamps.* The result should be a day-time interval but the data type has microsecond precision at the moment.
>  * *Changing existing time zone semantics.*
> It does not redefine NTZ/LTZ semantics, session time zone behavior, or SQL time zone rules.
>  * *A full rework of all connectors/storage formats*
> It does not promise end-to-end nanosecond support in every external system; connector-specific follow-ups may still be needed.
>  
> h1. Q3. How is it done today, and what are the limits of current practice?
> Today Spark SQL supports two built-in timestamp types, both at microsecond precision:
>  * TimestampType (TIMESTAMP WITH LOCAL TIME ZONE)
>  * TimestampNTZType (TIMESTAMP WITHOUT TIME ZONE)
> So Spark’s native timestamp model stops at 6 fractional digits.
>  
> For nanosecond data (for example Parquet TIMESTAMP(NANOS, ...)), current behavior is limited:
>  * Default behavior: Spark rejects it with an analysis error (for example: Illegal Parquet type: INT64 (TIMESTAMP(NANOS,true))).
>  * Legacy fallback (spark.sql.legacy.parquet.nanosAsLong=true): Spark reads it as LongType (raw integer), which drops timestamp semantics:
>  * no timestamp/date function behavior
>  * no time zone semantics
>  * no timestamp type safety
> In practice this means users either:
>  * down-convert data to microseconds before Spark, or
>  * keep nanosecond values as raw integers and do manual conversion logic, or
>  * switch to another engine for nanosecond timestamp workloads.
>  
> This is increasingly painful because nanosecond timestamps are common in data produced by systems like Pandas/PyArrow, Trino, ClickHouse, and DuckDB, and in domains such as market data, IoT telemetry, and tracing/observability pipelines.
>  
> h1. Q4. What is new in your approach and why do you think it will be successful?
> h2. What is new
>  # {*}Precision parameterization for timestamp types{*}
> Add TIMESTAMP_NTZ(n), TIMESTAMP_LTZ(n), and TIMESTAMP(n) (including alias forms) with explicit fractional precision, focused on 7 <= n <= 9.
>  # {*}New catalyst/API types for nanos-capable timestamps{*}
> Introduce parameterized nanos-aware types instead of overloading existing microsecond-only types.
>  * For TIMESTAMP_NTZ(n):
> case class TimestampNTZNanosType(precision: Int)
>  * For TIMESTAMP_LTZ(n):
> case class TimestampLTZNanosType(precision: Int)
>  # {*}Precise internal value model without changing core epoch semantics{*}
> Represent each value as:
>  * epoch microseconds (long)
>  * nanoseconds within the microsecond (0..999) This preserves existing Spark timestamp foundations while adding sub-micro precision.
>  # {*}End-to-end engine integration, not just parser support{*}
> The change is wired through parser/type system, expression evaluation, codegen, unsafe/container paths, and file-format handling paths needed for practical use.
> h2. Why this should succeed
>  # Low semantic risk: extends existing timestamp families instead of redefining them.
>  # Backward compatible path: existing microsecond types and behavior remain unchanged.
>  # Incremental implementation: narrow scope (7..9) and clear boundaries make rollout testable.
>  # Real interoperability value: directly addresses common nanos data sources where Spark currently fails or degrades to LongType.
>  # Operationally practical: keeps compact representation and reuses current execution architecture, so performance/regression risk is manageable.
>  
> h1. Q5. Who cares? If you are successful, what difference will it make?
> h2. Who cares
>  * Data engineering teams ingesting Parquet/warehouse data produced with nanosecond timestamps.
>  * Platform teams running mixed ecosystems (Spark + Trino/ClickHouse/DuckDB/Pandas/PyArrow).
>  * Domain teams where sub-micro timing matters (market data, telemetry, observability, IoT, CDC/event streams).
>  * Connector and table-format maintainers who currently need compatibility workarounds.
> h2. What difference success makes
>  # Interop works by default. Spark can read/use high-precision timestamp data as timestamp types, instead of failing or forcing LongType fallback.
>  # No more semantic loss workarounds. Users avoid manual bigint-to-timestamp conversions, custom UDF glue, and loss of timezone/type semantics.
>  # Correctness for high-frequency data. Distinct events within the same microsecond stay distinct; ordering and time-window logic become more reliable for nanos data.
>  # Lower migration friction to Spark. Teams can bring existing nanos datasets into Spark pipelines without pre-normalization to micros.
>  # Cleaner long-term type story. Spark gets an explicit precision model for timestamps (n) rather than implicit microsecond-only behavior, making schema contracts clearer across SQL and APIs.
>  
> h1. Q6. What are the risks?
> |Risk|Mitigation|
> |*User confusion* from more timestamp spellings (TIMESTAMP(n), TIMESTAMP_NTZ(n), TIMESTAMP_LTZ(n), plus WITHOUT TIME ZONE / WITH LOCAL TIME ZONE variants).|Keep today’s types as the default; require explicit n for the new behavior; document a small “cheat sheet” mapping SQL spelling -> semantics + precision.|
> |{*}Wrong results in shared datetime code paths{*}: functions/rules that implicitly assume microsecond-only timestamps (AnyTimestampType-style plumbing, codegen, optimizer rewrites) may mishandle nanosecond-capable values.|Systematic audit of shared abstractions (casts, comparisons, intervals, extract, codegen paths) plus focused regression tests on n ∈ [7,9].|
> |*Performance regressions* (wider projections, more branches in codegen/vectorized paths).|Benchmark common scans/joins/aggregations; keep fast paths for existing microsecond timestamps.|
> |*Range / overflow issues tied to widening.* Bugs where code accidentally converts to a single epochNanos long during promotion or builtins.|Document the representable range for the chosen internal representation; enforce bounds at cast boundaries; tests for edge instants.|
> |*Interop / external formats (Parquet/Iceberg/etc.):* external encodings may use epoch nanoseconds in int64, while Spark uses (micros, nanosWithinMicro). Conversion bugs are likely near boundaries and for pushdown predicates.|Conversions must be explicitly specified (including acceptable rounding/truncation) and covered by tests for the supported read/write paths you ship.|
>  
> h1. Q7. How long will it take?
> This estimate covers shipping of parameterized nanosecond-capable timestamps (7 <= n <= 9) with feature parity to existing TimestampType / TimestampNTZType, including parser, core type system, Parquet nanos support, encoders/converters, datetime utilities, cast matrix (interpreted + codegen), expression updates, type coercion, literals, and testing.
> Implementation should integrate with the Types Framework (SPARK-53504): register the new types through the centralized TypeOps / TypeApiOps (and storage/client Ops as applicable) instead of scattering one-off integration across dozens of files.
>  
> h3. Engineering estimate (person-weeks)
> |Area|pw|
> |Core type system (parameterization, physical dispatch for p<=6 vs p>=7, equality/hash/order + catalyst plumbing)|2|
> |SQL parser / AST (TIMESTAMP…(p) + aligned spellings)|1|
> |Parquet nanos read/write (incl. vectorized + non-vectorized, rebasing, legacy migration hooks, preview flags)|4|
> |Encoders / converters (framework-aligned)|1.5|
> |Datetime utils (nano parsing/conversion/arithmetic helpers)|1|
> |Cast matrix (interpreted + codegen, precision rules)|2|
> |Expression parity work (datetime builtins impacted)|5|
> |Type coercion / widening (TypeCoercion / AnsiTypeCoercion)|3|
> |Literals|1|
> |Testing (unit + Parquet + overflow/range + ANSI vs non-ANSI)|2|
> |Types Framework wiring + tests for the new type|1.5|
> |Total|25.0|
>  
> Calendar time (rule of thumb) ~25 person-weeks is about:
>  * ~6 months for one senior engineer (calendar), or
>  * ~7–9 weeks wall time for two engineers mostly dedicated,
> plus ~2–4 extra weeks calendar buffer for OSS review/CI churn and rebases.
>  
> h1. Q8. What are the mid-term and final “exams” to check for success?
> h3. Mid-term “exams”. Users can declare, load, and analyze nanosecond-capable timestamps without hacks:
>  # {*}SQL types are real, not experimental stubs.{*}
> Users can declare schemas with TIMESTAMP…(p) / TIMESTAMP_NTZ(p) / TIMESTAMP_LTZ(p) for p ∈ [7,9] (including equivalent WITHOUT TIME ZONE / WITH LOCAL TIME ZONE spellings), and those types round-trip through CREATE TABLE / CAST / DESCRIBE / explain plans in a predictable way.
>  # {*}Nanosecond timestamps stop degrading into “just BIGINT”.{*}
> Users can read common nanosecond Parquet inputs as timestamps, without being forced into LongType / legacy escape hatches for the supported paths you ship in this milestone.
>  # {*}Everyday analytics works on the new types.{*}
> For the shipped surface area, users can run typical workflows end-to-end: filters/joins/group-by keys, casts, timestamp arithmetic, extract/truncate-style operations.
>  # {*}Defaults unchanged for existing users.{*}
> Existing microsecond-first workloads behave as today unless users explicitly opt into p ∈ [7,9] or ingest nanosecond-native sources that trigger the new behavior.
>  
> h3. Final “exams”. Workflows are consistently usable, documented, and migration-ready at the level users expect from Spark timestamps today:
>  # {*}Parity exam vs existing Spark timestamps (user-visible).{*}
> For TimestampType / TimestampNTZType, users already expect a broad set of behaviors. For p ∈ [7,9], the shipped release must meet the same practical usability standard on the supported operations list (no “half the functions silently downgrade precision or fail inconsistently”).
>  # {*}Interop exam.{*}
> Users can run realistic pipelines: read nanosecond-rich Parquet, transform, and write/publish results without mandatory pre-processing outside Spark—within the explicitly documented guarantees (lossless round-trip everywhere is not required, but what is guaranteed must be testably true).
>  # {*}Migration exam.{*}
> There is a published migration path away from workflows that today rely on spark.sql.legacy.parquet.nanosAsLong (including what changes in schema types and what users must do).
>  # {*}Documentation exam.{*}
> Public docs answer: what p means, 7–9 scope, range/overflow behavior, ANSI vs non-ANSI, and known limitations—written so support/engineering can deflect repeated confusion.
>  
> h1. Appendix A. Proposed API Changes
> h2. A.1 SQL language
> Extend fractional-second precision (FSP) on timestamp families
> |*Surface*|*Proposed syntax*|*Role*|
> |NTZ nanos-capable|TIMESTAMP_NTZ(n)|Explicit NTZ with fractional precision {*}n{*}.|
> |Alias (NTZ)|TIMESTAMP(n) WITHOUT TIME ZONE|Same NTZ meaning as TIMESTAMP_NTZ(n) (aligned with SQL-style spelling).|
> |LTZ nanos-capable|TIMESTAMP_LTZ(n)|Explicit LTZ / “instant” timeline with n.|
> |Alias (LTZ)|TIMESTAMP(n) WITH LOCAL TIME ZONE|Same LTZ meaning as TIMESTAMP_LTZ(n).|
> |Session default timestamp form|TIMESTAMP(n)|Where Spark today resolves TIMESTAMP to LTZ or NTZ per configuration, n attaches FSP to that choice.|
> Backward compatibility
>  * Unparameterized forms keep today’s meaning:
>  * TIMESTAMP_NTZ -> existing TimestampNTZType (microsecond semantics).
>  * TIMESTAMP_LTZ / TIMESTAMP WITHOUT TIME ZONE / WITH LOCAL TIME ZONE combinations -> existing TimestampType / TimestampNTZType behavior unchanged when (n) is omitted.
>  * New behavior appears only when (n) is present (and, per SPIP scope, especially 7 <= n <= 9), mapping to the new catalyst types below - not silent widening of old types.
>  
> h2. A.2 Scala / Java DataType APIs
> New case classes:
> |Type|Purpose|Notes|
> |TimestampNTZNanosType(n: Int)|SQL TIMESTAMP_NTZ(n) (nanosecond-capable NTZ).|Present on this branch: sql / typeName like TIMESTAMP_NTZ(n; n in 0..9 today in code—SPIP may narrow product scope to 7..9 while keeping storage general.|
> |TimestampNanosType(n: Int)|SQL TIMESTAMP_LTZ(n) (nanosecond-capable LTZ).|Proposed companion to NTZ; mirrors the same FSP parameter and external/Java mapping pattern.|
> Backward compatibility
>  * Existing singleton types TimestampNTZType and TimestampType remain the defaults for unparameterized SQL and for older serialized schemas that do not carry n.
>  * New types are additive; code that pattern-matches only on TimestampType / TimestampNTZType continues to compile but must be reviewed for parity paths when TimestampNanosType / TimestampNTZNanosType appear.
>  
> h2. A.3 Compatibility summary
> |Direction|Expectation|
> |Old → new|Opt-in via TIMESTAMP_* (n) / new DataTypes; no change for legacy microsecond types unless users choose nanosecond-capable types.|
> |New → old|Downgrades may truncate/round per FSP rules; must be documented and tested (ANSI throw vs non-ANSI null where applicable).|
> |Cross-version|Schemas with nanosecond-capable types require a Spark version that understands them; older engines must reject or require migration tools—not silently coerce.|
>  
> h1. Appendix B. Design Sketch.
> A value for nanosecond-capable NTZ (and the same pair for LTZ):
>  
>  * {*}epochMicros{*}: Long — signed epoch microseconds (same grid as TimestampType / TimestampNTZType today).
>  * {*}nanosOfMicro{*}: Short in [0, 999] — remaining nanoseconds inside that microsecond bucket.
> {*}Invariant{*}: the pair is always normalized so *nanosOfMicro* stays in range; excess carries into *epochMicros* with Math.addExact / floor-div where needed.
> h4. Why this split (vs a single Long epoch-nanos counter):
>  * {*}Range{*}: *epochMicros* as Long keeps the calendar reach in the same ballpark as today’s microsecond timestamps. A single INT64 epoch-nanoseconds field has a much smaller representable year range; Spark can avoid that user-visible cliff for the micros timeline part.
>  * *Interop/conversion cost:* most existing Spark datetime math is already microsecond-grained; incremental changes upgrade paths by composing micro ops + cheap nano remainder, instead of forcing every operator to immediately normalize to nanoseconds-since-epoch.
>  * *Deterministic normalization:* *nanosOfMicro* is a small bounded correction term - easy to audit in casts/parsers vs unconstrained nano arithmetic everywhere.
>  
> TimestampNTZNanosType (and similar TimestampLTZNanosType) is the schema-level description of TIMESTAMP_NTZ(p). The values for this type are not “a struct of two children in the row” at runtime; the companion internalStructType exists for metadata only (historical/compat hooks). Execution uses the normalized logical pair epoch micros + nanoseconds within that micro via org.apache.spark.unsafe.types.TimestampNTZNanos.
>  
> /**
>  * Timestamp without time zone with fractional-second precision up to
>  * nanoseconds (9 decimal digits)
>  */
> @Unstable
> case class TimestampNTZNanosType(precision: Int) extends DatetimeType {
>   if (precision < 0 || precision > 9) {
>     throw DataTypeErrors.unsupportedTimestampPrecisionError(precision)
>   }
>  
>   /**
>    * Default size used by Spark for row-size estimation.
>    * Nanosecond-capable NTZ values are represented logically as epoch
>    * microseconds (`Long`, 8 bytes) plus nanoseconds within that micro
>    * (`Short`, 2 bytes). Size estimation sums those fixed logical
>    * parts (10 bytes). Physical encoding in [[org.apache.spark.sql.catalyst.expressions.UnsafeRow]]
>    * uses a separate layout (one fixed pointer word plus a variable-length payload).
>    */
>   override def defaultSize: Int = 10
>  
>   override def typeName: String = s"timestamp_ntz($precision)"
>   override def sql: String = s"TIMESTAMP_NTZ($precision)"
>   private[spark] override def asNullable: TimestampNTZNanosType = this
> }
>  
> h1. Appendix C. Rejected Designs
> This SPIP converged on a logical pair: epoch microseconds (Long) + nanoseconds within that micro (0..999, typically Short) with a single normalization rule. Other encodings were considered and set aside for the reasons below.
> h2. C.1 Epoch nanoseconds in a single Long
>  * {*}Reference{*}: related direction in the broader nanosecond-timestamp SPIP / design thread ([Google Doc: SPIP: Support NanoSecond Timestamp Types|https://docs.google.com/document/d/1Q5u1whAO_KcT6d4dFFaIMy_S3RoQEo4Znwz2U-nbhls/edit?tab=t.0#heading=h.xk16mmomv6il]).
>  * {*}Idea{*}: store the instant as signed 64-bit nanoseconds since Unix epoch.
>  * *Why rejected* as the primary internal model for Spark’s timestamp execution:
>  * Much smaller representable calendar range in INT64 nanoseconds than INT64 microseconds (a well-known “range cliff” vs today’s microsecond timestamps).
>  * Higher integration cost with the existing engine, which is overwhelmingly organized around microsecond-grained datetime rules; everything would either convert constantly or risk drift.
>  * Not compatible with the SQL standard
> h2. C.2 Seconds + nanos of second
>  * *Idea:* decompose time into whole seconds and 0..999_999_999 nanos within the second.
>  * *Why rejected:*
>  * Mismatches Spark’s native microsecond “currency” (long micros) used across most of Catalyst; forces pervasive rescaling and increases codegen/interpreted drift risk.
>  * Second-bucket arithmetic is awkward for interval APIs and microsecond-legacy behavior (many paths think in micros, not “seconds + intra-second nanos”).
> h2. C.3 Days + nanos within the day
>  * {*}Idea{*}: pack calendar date as day index and time-of-day as nanoseconds within the day.
>  * *Why rejected:*
>  * Not aligned with how Spark’s datetime system reasons about instants (epoch micros + zone rules, legacy rebasing, Julian/Gregorian transitions, etc.).
>  * Day-boundary corner cases (leap seconds notwithstandings, DST is less relevant to NTZ/LTZ storage but the model still composes poorly with “micros since epoch” execution).
> h2. C.4 Nanos-from-epoch + Byte “high extension”
>  * {*}Idea{*}: keep nanosecond resolution but add an extra byte to extend effective range beyond pure INT64 nanos limits.
>  * *Why rejected:*
>  * Non-standard, harder to explain, and complicates every operator (suddenly values are a 2-part variable-precision integer in the hot path).
>  * Interoperability pain: external systems and file formats won’t natively match the “byte extension” scheme; you still convert at boundaries.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@spark.apache.org
For additional commands, e-mail: issues-help@spark.apache.org