Skip to content

feat(operator): read a timestamp cast over the years the engine reaches - #8597

Open
kz930 wants to merge 3 commits into
apache:mainfrom
kz930:fix/8595-timestamp-cast-range
Open

kz930 wants to merge 3 commits into
apache:mainfrom
kz930:fix/8595-timestamp-cast-range

Conversation

@kz930

@kz930 kz930 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

Type Casting to a timestamp read text with pd.to_datetime(col, errors="coerce") in the exported script. That is wrong in two ways, and both empty a cell the run itself filled.

pandas parses into nanoseconds, which reach only 1677-09-21 to 2262-04-11. The engine holds a java.sql.Timestamp, where 2500-01-01 00:00:00 is an ordinary moment: it reads it, and the exported script answered with an empty cell. The same for 1500-06-15 08:30:00 and 9999-12-31 23:59:59.

pandas also infers one format for the whole column and coerces every row that does not match it. The engine hands DateParserUtils one field at a time, so a row states its own format. A column holding 2024-03-05 14:09:07 and March 5, 2024 kept the first and emptied the second.

The text branch now reads the column cell by cell and holds the result at microsecond resolution, which covers the years the engine covers. It still coerces: the engine accepts a set of formats no single pandas call states, so text neither side can read leaves an empty cell rather than ending an exported run halfway. That part is unchanged, and a test now says so.

The number branch is left alone. A LONG is read as epoch milliseconds and has the same window, but it also disagrees about the calendar before 1582-10-15, where new Timestamp(long) is Julian and pandas is proleptic Gregorian. Widening the window there would answer with a moment eight days off instead of an empty cell, so what that branch should do is a separate decision. The issue records it.

Any related issues, documentation, discussions?

Closes #8595. #8480 closed the same nanosecond gap for Extract Date/Time Fields, which parses strictly and could take the one-line form this operator cannot.

How was this PR tested?

Two tests in TypeCastingOpDescSpec, both running the generated Python and comparing cell by cell against AttributeTypeUtils.parseField, which is how that spec already compares the other casts.

The first reads eight moments in one column: three ordinary ones written in three different formats, the two rows either side of the nanosecond edge, and 2500-01-01, 1500-06-15 and 9999-12-31. It fails on the old generator, which empties five of the eight. The second says the cast still answers unreadable text with an empty cell, where the engine refuses it.

The whole WorkflowOperator module passes, 2567 tests. scalafmtCheckAll and scalafixAll --check are clean.

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Claude Opus 5)

pandas parses into nanoseconds, which reach only 1677 to 2262, so the exported
script emptied every moment outside that window where the engine holds a
java.sql.Timestamp and reads it like any other. It also inferred one format for
the whole column, so a row written differently from the first one was emptied
too, where the engine hands DateParserUtils a field at a time.

The text branch now reads the column cell by cell at microsecond resolution.
Text neither side can read still leaves an empty cell rather than ending the
run, which is what the cast has always promised.

Closes apache#8595

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • No candidates found from git blame history.

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

🟢 4 better · 🔴 1 worse · ⚪ 10 noise (<±5%) · 0 without baseline

Compared against main 5264df2 benchmarked on this same runner, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.

Dashboard · Run

config throughput MB/s latency max Δ latest / 7d
🟢 bs=10 sw=10 sl=64 486 0.297 19,196/28,859/28,859 us 🟢 -9.0% / 🔴 +82.6%
🔴 bs=100 sw=10 sl=64 965 0.589 102,208/135,723/135,723 us 🟢 -9.9% / 🔴 +24.8%
bs=1000 sw=10 sl=64 1,160 0.708 854,478/987,316/987,316 us ⚪ within ±5% / 🟢 -14.5%
Baseline details

Latest main 5264df2 from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 486 tuples/sec 474 tuples/sec 767.43 tuples/sec +2.5% -36.7%
bs=10 sw=10 sl=64 MB/s 0.297 MB/s 0.29 MB/s 0.468 MB/s +2.4% -36.6%
bs=10 sw=10 sl=64 p50 19,196 us 19,312 us 12,880 us -0.6% +49.0%
bs=10 sw=10 sl=64 p95 28,859 us 31,700 us 15,801 us -9.0% +82.6%
bs=10 sw=10 sl=64 p99 28,859 us 31,700 us 19,767 us -9.0% +46.0%
bs=100 sw=10 sl=64 throughput 965 tuples/sec 1,005 tuples/sec 988.73 tuples/sec -4.0% -2.4%
bs=100 sw=10 sl=64 MB/s 0.589 MB/s 0.613 MB/s 0.603 MB/s -3.9% -2.4%
bs=100 sw=10 sl=64 p50 102,208 us 94,044 us 102,684 us +8.7% -0.5%
bs=100 sw=10 sl=64 p95 135,723 us 150,699 us 108,712 us -9.9% +24.8%
bs=100 sw=10 sl=64 p99 135,723 us 150,699 us 118,731 us -9.9% +14.3%
bs=1000 sw=10 sl=64 throughput 1,160 tuples/sec 1,202 tuples/sec 1,022 tuples/sec -3.5% +13.5%
bs=1000 sw=10 sl=64 MB/s 0.708 MB/s 0.734 MB/s 0.624 MB/s -3.5% +13.5%
bs=1000 sw=10 sl=64 p50 854,478 us 825,197 us 999,086 us +3.5% -14.5%
bs=1000 sw=10 sl=64 p95 987,316 us 970,047 us 1,037,033 us +1.8% -4.8%
bs=1000 sw=10 sl=64 p99 987,316 us 970,047 us 1,066,123 us +1.8% -7.4%
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,411.11,200,128000,486,0.297,19195.84,28859.14,28859.14
1,100,10,64,20,2071.69,2000,1280000,965,0.589,102207.62,135723.30,135723.30
2,1000,10,64,20,17239.71,20000,12800000,1160,0.708,854478.01,987315.79,987315.79

@codecov-commenter

codecov-commenter commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.77%. Comparing base (2ab8ee0) to head (ca67ce1).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8597      +/-   ##
============================================
- Coverage     92.78%   92.77%   -0.01%     
  Complexity     4898     4898              
============================================
  Files          1236     1236              
  Lines         52121    52131      +10     
  Branches       6405     6407       +2     
============================================
+ Hits          48359    48365       +6     
- Misses         2186     2189       +3     
- Partials       1576     1577       +1     
Flag Coverage Δ *Carryforward flag
access-control-service 71.78% <ø> (ø)
agent-service 99.32% <ø> (ø) Carriedforward from c074933
amber 88.59% <ø> (-0.02%) ⬇️
computing-unit-managing-service 55.20% <ø> (ø)
config-service 87.37% <ø> (ø)
file-service 81.53% <ø> (ø)
frontend 96.58% <ø> (ø) Carriedforward from c074933
notebook-migration-service 83.73% <ø> (ø)
pyamber 98.48% <ø> (ø) Carriedforward from c074933
workflow-compiling-service 74.09% <ø> (ø)

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@carloea2 carloea2 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.

The new timestamp helper crashes on 2024-03-05T14:09:07Z. I ran the generated helper with pandas 2.2.3. Parsing keeps the timezone, then astype rejects the conversion to a timestamp without a timezone. The previous code accepts this value. Please handle timestamps with a timezone and add a test with Z and an explicit offset.

The cast stopped on one. A reading that carries a zone cannot be converted to a
column that holds none, so `2024-03-05T14:09:07Z` ended the cast where the
previous generator had read it, and an explicit offset did the same.

The engine reads the offset and keeps no zone for it: DateParserUtils parses the
reading and java.sql.Timestamp holds the wall clock of the machine's own zone,
so `...T14:09:07Z` is 06:09:07 where the machine is eight hours behind UTC. The
helper now does that, which is also what the epoch-milliseconds branch beside it
already did.

The two spellings join the cases the timestamp test compares against
parseField cell by cell, so what they should read is taken from the engine
rather than written down and the pair says the same thing in any zone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kz930

kz930 commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in c07493387. The helper now does what the engine does with an offset — DateParserUtils reads it and java.sql.Timestamp keeps no zone, so the moment is held as the wall clock of the machine's own zone (...T14:09:07Z is 06:09:07 eight hours behind UTC) — and both spellings joined the cases the test compares against parseField cell by cell, so the expectation comes from the engine rather than being written down.

@carloea2 carloea2 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.

The timezone crash is fixed. I ran the current helper with Z, an explicit offset, year 2500, and null. Those checks passed.

@carloea2 carloea2 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.

Reproduced locally with the generated Python code. One correctness issue below. I did not run a frontend workflow or the full Scala suite.

| if pd.api.types.is_datetime64_any_dtype(s):
| if getattr(s.dtype, "tz", None) is not None:
| s = s.dt.tz_convert(tzlocal()).dt.tz_localize(None)
| return s.astype("datetime64[us]")

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.

This also converts existing timestamp columns to microseconds. I tested 14:09:07.123456789 and 14:09:07.123456001: both become 14:09:07.123456, while the previous export keeps them distinct. Please preserve the input resolution for timestamp-to-timestamp casts and add a nanosecond test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed and fixed in ca67ce1. The astype was meant for the text path and was also catching an already-parsed column; that branch now returns the column at the resolution it arrived in, and a test compares your two values against parseField cell by cell.

The cast narrowed one. A column the engine already holds as a moment went
through the same astype as freshly parsed text, so 14:09:07.123456789 and
14:09:07.123456001 both came back as .123456 and two rows the run tells apart
became one. The previous generator left such a column alone.

parseField hands a java.sql.Timestamp back untouched and that class counts
nanoseconds, so there is nothing for this branch to decide: the column is
returned at the resolution it arrived in, zoned or not. The text branch below
is unchanged and still reads into microseconds, which is what the years past
the nanosecond edge need.

A test casts a timestamp column to timestamp and compares the two values cell
by cell against parseField, which is how the rest of the spec compares a cast.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Casting text to a timestamp empties a moment the engine reads

3 participants