Skip to content

Writing a Timestamp in the last ~26h of year 9999 panics during Min/Max stats #8860

Description

@tobias-fire

What happens

When a Timestamp column holds a value greater than jiff's Timestamp maximum (253402207200 s = 9999-12-30T22:00:00Z), VortexWriteOptions::write panics (via vortex_panic!) while computing the Min/Max statistic. Vortex builds an extension Scalar for the min/max value and validates it through jiff; for values in the last ~26 hours of year 9999 that validation fails and surfaces as a panic rather than an error. Arrow, Parquet, and many SQL engines allow timestamps up to 9999-12-31T23:59:59, so this can be triggered by ordinary in-range data.

Version: vortex 0.78.0.

Minimal repro (only depends on vortex = "=0.78.0"):

use std::sync::LazyLock;
use vortex::VortexSessionDefault;
use vortex::array::arrays::{ExtensionArray, StructArray};
use vortex::array::extension::datetime::{TimeUnit, Timestamp};
use vortex::array::{ArrayRef, IntoArray};
use vortex::buffer::{ByteBufferMut, buffer};
use vortex::dtype::Nullability;
use vortex::file::WriteOptionsSessionExt;
use vortex::io::runtime::BlockingRuntime;
use vortex::io::runtime::current::CurrentThreadRuntime;
use vortex::io::session::RuntimeSessionExt;
use vortex::session::VortexSession;

static RUNTIME: LazyLock<CurrentThreadRuntime> = LazyLock::new(CurrentThreadRuntime::new);
static SESSION: LazyLock<VortexSession> =
    LazyLock::new(|| VortexSession::default().with_handle(RUNTIME.handle()));

fn write_one_timestamp(ts_micros: i64) -> usize {
    let ext = Timestamp::new(TimeUnit::Microseconds, Nullability::NonNullable).erased();
    let storage: ArrayRef = buffer![ts_micros].into_array();
    let ts: ArrayRef = ExtensionArray::new(ext, storage).into_array();
    let table = StructArray::from_fields(&[("t", ts)]).unwrap().into_array();
    let mut buf = ByteBufferMut::empty();
    RUNTIME.block_on(async { SESSION.write_options().write(&mut buf, table.to_array_stream()).await })
        .expect("write should succeed");
    buf.len()
}

fn main() {
    const IN_RANGE:   i64 = 253_402_207_200_000_000; // 9999-12-30T22:00:00Z (jiff max) — OK
    const OVER_RANGE: i64 = 253_402_214_400_000_000; // 9999-12-31T00:00:00Z — panics
    println!("in-range: wrote {} bytes", write_one_timestamp(IN_RANGE));
    println!("over-range: wrote {} bytes", write_one_timestamp(OVER_RANGE)); // panics here
}

Output:

in-range  (9999-12-30 22:00:00Z): wrote 2676 bytes
over-range (9999-12-31 00:00:00Z): writing...
thread 'main' panicked at vortex-error-0.78.0/src/lib.rs:347:
unable to construct an extension `Scalar`:
  Other error: Invalid timestamp scalar: parameter 'Unix timestamp seconds' is not in the required range of -377705023201..=253402207200

The value at the limit (9999-12-30T22:00:00Z) writes fine; the same column two hours later aborts, which points to the jiff range boundary.

Root cause

  • aggregate_fn/fns/min_max/extension.rs::accumulate_extension computes min/max on the i64 storage, then wraps the results with the infallible Scalar::extension_ref (internally Scalar::try_new(...).vortex_expect("unable to construct an extension Scalar")).
  • extension/datetime/timestamp.rs::unpack_native validates via jiff::Timestamp::UNIX_EPOCH.checked_add(span), and jiff intentionally reserves headroom (so a Timestamp stays convertible to a civil datetime in any timezone), so it can't represent the final ~26h of year 9999.
  • The fallible result becomes a panic, which aborts the writer — in our case across a C FFI boundary, so it takes down the whole process.

Expected behavior

Ideally, support the full timestamp range that Arrow and Parquet allow (through the end of 9999-12-31). Alternatively, skip the Min/Max statistic when a value can't be represented as an extension scalar — the data itself round-trips fine, so dropping the stat for that column/chunk is preferable to a panic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions