Skip to content

[Data] Write Delta timestamps as microseconds in write_delta - #66907

Open
HirokiNariyoshi wants to merge 5 commits into
ray-project:masterfrom
HirokiNariyoshi:fix/delta-sink-timestamp-us
Open

HirokiNariyoshi wants to merge 5 commits into
ray-project:masterfrom
HirokiNariyoshi:fix/delta-sink-timestamp-us

Conversation

@HirokiNariyoshi

@HirokiNariyoshi HirokiNariyoshi commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

DeltaDatasink writes Parquet with PyArrow directly and never converts
timestamps to microseconds, which deltalake's own write_deltalake does.
Any s, ms, or ns timestamp, whether top-level, nested in a struct,
list, or map, or dictionary-encoded, therefore breaks the write:

  • deltalake 1.5.0 (Ray's pinned version): the commit fails with
    Invalid data type for Delta Lake after the workers have already written
    their Parquet files, leaving those files orphaned.
  • deltalake 1.6.x: ns commits a log schema of us over ns files, so the
    table's schema disagrees with its Parquet files. Timezone-aware ns
    commits a table with the timestampNanos reader feature, which the
    deltalake reader rejects.

This PR casts every timestamp, including nested and dictionary-encoded ones,
to timestamp[us] (timezone-aware ones in UTC) on each worker before
writing. It also converts the schema captured in on_write_start, which is
what an empty write commits. Sub-microsecond precision is truncated, since
Delta can't store it; out-of-range values still raise. Nanosecond timestamps are always
truncated to us, even if deltalake.enable_nanosecond_timestamps() (experimental, deltalake ≥ 1.6)
has been called, since Delta's stable timestamp type is microseconds.

The change is limited to DeltaDatasink and its tests.

Repro on master with deltalake 1.5.0:

import pyarrow as pa, ray
t = pa.table({"t": pa.array([1704110400_123456789], pa.timestamp("ns"))})
ray.data.from_arrow(t).write_delta("/tmp/delta_ts")
# Schema error: Invalid data type for Delta Lake: Timestamp(ns)

Related issues

None. I found this while reading DeltaDatasink. I searched open and closed
issues and PRs for write_delta, DeltaDatasink, delta_datasink,
timestampNanos, and "Invalid data type for Delta Lake", and found no
duplicate.

Additional information

Tests run locally:

  • pytest python/ray/data/tests/datasource/test_write_delta.py: 112 passed
    on deltalake 1.5.0 and on 1.6.1.
  • The 11 new tests (every unit with and without a timezone; struct, list,
    large_list, fixed_size_list, map, and dictionary-encoded columns; an ns
    append onto a us table; an empty dataset; out-of-range values; timestamp map keys)
    fail on master and pass with this change.
  • The new helpers also work on pyarrow 15.0.0, the lowest version the test
    file allows.
  • pre-commit run --files <changed files>: pass.

AI assistance was used to draft the change.
I reviewed every changed line and ran the tests above.

Copilot AI balanced review requested due to automatic review settings October 10, 2026 00:57
@HirokiNariyoshi
HirokiNariyoshi requested a review from a team as a code owner October 10, 2026 00:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@HirokiNariyoshi
HirokiNariyoshi marked this pull request as draft October 10, 2026 00:57

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces automatic conversion of PyArrow timestamp types (including nested ones in structs, lists, and maps) to microsecond precision (and UTC for timezone-aware ones) when writing to Delta Lake, ensuring compatibility with Delta Lake's supported timestamp formats. The reviewer suggested extending this conversion to handle dictionary-encoded timestamp types to make the implementation more robust.

Comment thread python/ray/data/_internal/datasource/delta_datasink.py
HirokiNariyoshi and others added 3 commits October 9, 2026 21:22
Signed-off-by: Hiroki Nariyoshi <hnariyos@uwaterloo.ca>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: Hiroki Nariyoshi  <narihiro.hiroki@gmail.com>
Signed-off-by: Hiroki Nariyoshi <hnariyos@uwaterloo.ca>
Signed-off-by: Hiroki Nariyoshi <hnariyos@uwaterloo.ca>
@HirokiNariyoshi
HirokiNariyoshi force-pushed the fix/delta-sink-timestamp-us branch from c404171 to 7b07e05 Compare October 10, 2026 01:23
@HirokiNariyoshi
HirokiNariyoshi marked this pull request as ready for review October 10, 2026 01:44

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces automatic conversion of PyArrow timestamp types, including nested ones, to microsecond precision (and UTC for timezone-aware ones) when writing to Delta Lake. This ensures compatibility with Delta Lake's supported timestamp formats. Feedback points out a bug in the fixed-size list conversion logic where PyArrow would raise a ValueError if a Field object is passed with a specified list size, and suggests using the resolved DataType directly.

Comment thread python/ray/data/_internal/datasource/delta_datasink.py
@ray-gardener ray-gardener Bot added data Ray Data-related issues community-contribution Contributed by the community labels Oct 10, 2026
@Yin-1022

Copy link
Copy Markdown

I've tested _cast_to_delta_schema() with an out-of-range timestamp[s] value (9223372036855), and it correctly raises ArrowInvalid instead of silently overflowing. Just sharing the result in case it is helpful.

Since the docstring explicitly guarantees this behavior, would it be worth adding a regression test for this boundary case? It could help prevent accidental changes to the overflow behavior in future refactoring.

Here's my attempt on that regression test

def test_timestamp_overflow_rejected():
    import pyarrow as pa

    from ray.data._internal.datasource.delta_datasink import (
        _cast_to_delta_schema,
    )

    table = pa.table({
        "t": pa.array(
            [9_223_372_036_855],
            type=pa.timestamp("s"),
        ),
    })

    with pytest.raises(pa.ArrowInvalid, match="out of bounds"):
        _cast_to_delta_schema(table)

Also, _to_delta_type() recursively converts both map keys and values, but the nested timestamp test only covers timestamp values with string keys. Would a timestamp-keyed map also be a supported case for this conversion? If so, could a test for that path be added, or clarify whether Delta Lake supports it?

None of them are blockers, just suggestion for test coverage.

…elta

Signed-off-by: Hiroki Nariyoshi <hnariyos@uwaterloo.ca>
@HirokiNariyoshi

Copy link
Copy Markdown
Contributor Author

I've tested _cast_to_delta_schema() with an out-of-range timestamp[s] value (9223372036855), and it correctly raises ArrowInvalid instead of silently overflowing. Just sharing the result in case it is helpful.

Since the docstring explicitly guarantees this behavior, would it be worth adding a regression test for this boundary case? It could help prevent accidental changes to the overflow behavior in future refactoring.

Here's my attempt on that regression test

def test_timestamp_overflow_rejected():
    import pyarrow as pa

    from ray.data._internal.datasource.delta_datasink import (
        _cast_to_delta_schema,
    )

    table = pa.table({
        "t": pa.array(
            [9_223_372_036_855],
            type=pa.timestamp("s"),
        ),
    })

    with pytest.raises(pa.ArrowInvalid, match="out of bounds"):
        _cast_to_delta_schema(table)

Also, _to_delta_type() recursively converts both map keys and values, but the nested timestamp test only covers timestamp values with string keys. Would a timestamp-keyed map also be a supported case for this conversion? If so, could a test for that path be added, or clarify whether Delta Lake supports it?

None of them are blockers, just suggestion for test coverage.

Thanks for testing this! Both addressed in d7a5a2e

test_timestamp_conversion_rejects_out_of_range_values, based on your test, using 9,223,372,036,855 s, the smallest whole second whose microsecond value overflows int64.

Map keys are converted, and timezone-aware keys commit fine, so I added a test for that. Naive timestamp keys can’t be written yet, independent of this PR. when a naive timestamp appears only as a map key, deltalake doesn’t add the timestampNtz feature, so the commit fails even with microsecond keys. write_deltalake fails the same way on 1.5.0 and 1.6.1.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution Contributed by the community data Ray Data-related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants