Skip to content

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

Merged
aglinxinyuan merged 28 commits into
apache:mainfrom
kz930:fix/8595-timestamp-cast-range
Nov 1, 2026
Merged

aglinxinyuan merged 28 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. Text no format reads now ends the run, as the engine's parseField ends its own. The script raises the engine's message, Failed to parse type str to Timestamp, where it used to leave the cell empty. dateutil reads every format the verification hands the engine, so the two refuse the same text.

Review and the verification's edge values found more places where the cast parted from the engine:

  • A reading that states an offset, such as Z or +05:30, stopped the cast. The engine moves it to the machine's zone and keeps that wall clock, and the script now does the same.
  • A column that already held timestamps went through the text parse and lost its nanoseconds. parseField returns a java.sql.Timestamp untouched, so the script now hands such a column back at the resolution it arrived in.
  • Text with digits past the millisecond kept them. DateParserUtils reads into a java.util.Date, which counts milliseconds, so the script now cuts there too.
  • Casting to text wrote a value the way Python's str() does. Java writes a double past 1e7 or under 1e-3 in E notation, a boolean in lower case, and a timestamp with its nanoseconds and at least .0. A shared _texera_java_text now writes each one as Java's toString does.
  • A double column holding a NaN as a value, as an Arrow or Parquet source hands one over, lost it. The casts asked each cell whether it was empty, and pandas hands an empty cell over as NaN too, so a stored NaN came out null where the engine writes NaN as text, 0 as an integer and true as a boolean. The casts now read empty cells from the column's own mask, and the double cast builds a nullable Float64, so a NaN read from text stays one through the next cast. A double past an integer's bounds now saturates as toInt and toLong do, where int() raised on an infinity.
  • A moment was read with dateutil's tzlocal(), which knows only today's two offsets, so an instant from before a zone changed its rules came out an hour or more off on every platform. The script now reads the machine's zone by its IANA name and, before 1900, keeps today's standard offset as java.util.Date does.
  • On Windows, localtime refuses an instant before 1970, which stopped the cast. The same helper now falls back to the zone's standard offset there.

That Float64 column changed what a Python operator after this cast is handed. pyamber hands a Python operator a DOUBLE column as float64, where a NaN and a null are one value, and writes a NaN out as null. Given Float64 instead, Sort ordered a NaN apart from the nulls, a chart drew points the engine dropped, and a passed-through NaN stayed NaN. StandaloneCodeGenerator gains standaloneInputAdapter(), and for a Python operator the translator feeds the fragment a float64 copy of each input, leaving the upstream frame as it was.

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.

#8341, #8704, #8705 and #8709 call the timestamp and text helpers this adds, so this PR merges before them.

How was this PR tested?

Eight tests in TypeCastingOpDescSpec run the generated Python and compare it cell by cell against AttributeTypeUtils.parseField. They are tagged to run in the integration job, which has pandas.

  • The text-to-timestamp test reads moments in three formats, the rows either side of the nanosecond edge, 2500-01-01, 1500-06-15, 9999-12-31, readings with Z and +05:30, and readings past the millisecond. It fails on the old generator.
  • One test gives a column that already holds timestamps and checks its nanoseconds are kept.
  • The cast-to-string test covers a double at 1e7 and 1e-4.
  • One test checks that unreadable text ends the script with the engine's message, as the engine refuses it.
  • Two tests cast a column holding a stored NaN, both infinities and an empty cell to each type and compare against AttributeTypeUtils, and check that a NaN read from text survives a second cast.
  • Two tests read moments from America/Mexico_City in 1899, 1921, 1931 and 2021, at the offset the zone had then, and the same moments with localtime refusing those before 1970.
  • Three tests in WorkflowToPythonTranslatorSpec check that a Python operator reads a converted copy of its input and an operator the engine runs on the JVM reads its upstream as it is. Two of them fail without the change.

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

🟢 2 better · 🔴 7 worse · ⚪ 6 noise (<±5%) · 0 without baseline

Compared against main 7b8422e 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 740 0.451 12,965/22,176/22,176 us 🔴 +19.7% / 🔴 +57.4%
🟢 bs=100 sw=10 sl=64 1,805 1.101 54,761/61,632/61,632 us 🟢 -26.1% / 🟢 +53.8%
🔴 bs=1000 sw=10 sl=64 2,104 1.284 463,525/629,266/629,266 us 🔴 +22.4% / 🟢 +74.7%
Baseline details

Latest main 7b8422e from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 740 tuples/sec 820 tuples/sec 888.52 tuples/sec -9.8% -16.7%
bs=10 sw=10 sl=64 MB/s 0.451 MB/s 0.5 MB/s 0.542 MB/s -9.8% -16.8%
bs=10 sw=10 sl=64 p50 12,965 us 10,828 us 11,467 us +19.7% +13.1%
bs=10 sw=10 sl=64 p95 22,176 us 19,017 us 14,087 us +16.6% +57.4%
bs=10 sw=10 sl=64 p99 22,176 us 19,017 us 17,519 us +16.6% +26.6%
bs=100 sw=10 sl=64 throughput 1,805 tuples/sec 1,754 tuples/sec 1,174 tuples/sec +2.9% +53.8%
bs=100 sw=10 sl=64 MB/s 1.101 MB/s 1.07 MB/s 0.716 MB/s +2.9% +53.7%
bs=100 sw=10 sl=64 p50 54,761 us 53,259 us 89,952 us +2.8% -39.1%
bs=100 sw=10 sl=64 p95 61,632 us 83,425 us 96,426 us -26.1% -36.1%
bs=100 sw=10 sl=64 p99 61,632 us 83,425 us 110,188 us -26.1% -44.1%
bs=1000 sw=10 sl=64 throughput 2,104 tuples/sec 2,128 tuples/sec 1,204 tuples/sec -1.1% +74.7%
bs=1000 sw=10 sl=64 MB/s 1.284 MB/s 1.299 MB/s 0.735 MB/s -1.2% +74.7%
bs=1000 sw=10 sl=64 p50 463,525 us 468,494 us 886,711 us -1.1% -47.7%
bs=1000 sw=10 sl=64 p95 629,266 us 513,948 us 928,644 us +22.4% -32.2%
bs=1000 sw=10 sl=64 p99 629,266 us 513,948 us 972,356 us +22.4% -35.3%
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,270.40,200,128000,740,0.451,12964.52,22175.70,22175.70
1,100,10,64,20,1108.24,2000,1280000,1805,1.101,54761.23,61631.97,61631.97
2,1000,10,64,20,9507.40,20000,12800000,2104,1.284,463524.82,629266.39,629266.39

@codecov-commenter

codecov-commenter commented Sep 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.33333% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.18%. Comparing base (7b8422e) to head (1ed5465).

Files with missing lines Patch % Lines
.../amber/translator/WorkflowToPythonTranslator.scala 72.72% 0 Missing and 6 partials ⚠️
...exera/amber/operator/StandaloneCodeGenerator.scala 42.85% 4 Missing ⚠️
...ache/texera/amber/operator/StandaloneHelpers.scala 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##               main    #8597   +/-   ##
=========================================
  Coverage     92.18%   92.18%           
- Complexity     5051     5056    +5     
=========================================
  Files          1286     1286           
  Lines         55937    55963   +26     
  Branches       7140     7146    +6     
=========================================
+ Hits          51563    51589   +26     
+ Misses         2612     2609    -3     
- Partials       1762     1765    +3     
Flag Coverage Δ *Carryforward flag
access-control-service 77.44% <ø> (ø)
agent-service 99.16% <ø> (ø) Carriedforward from 7b8422e
amber 86.91% <37.50%> (-0.03%) ⬇️
computing-unit-managing-service 64.02% <ø> (ø)
config-service 87.61% <ø> (ø)
file-service 82.29% <ø> (ø)
frontend 96.48% <ø> (ø) Carriedforward from 7b8422e
notebook-migration-service 83.77% <ø> (ø)
pyamber 98.54% <ø> (+0.06%) ⬆️ Carriedforward from 7b8422e
workflow-compiling-service 77.95% <72.72%> (+3.70%) ⬆️

*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.

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>

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

Retested ca67ce1. Timezone inputs, extended years and nanosecond precision pass. The previous findings are fixed. Looks good.

… write text as Java's toString does

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

kz930 commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

@carloea2 Since your approval, text read into a timestamp is cut to the millisecond as DateParserUtils does, and a cast to text writes a value as Java's toString does.

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

Retested with JDK 17.0.20.1. The helper still writes 1.0E23 where the engine writes 9.999999999999999E22. The JDK 19 form would keep this mismatch. We need maintainer agreement on the supported formatting contract before accepting that difference.

kz930 and others added 2 commits October 12, 2026 15:10
The helper writes a double's shortest digits, as Double.toString does
from JDK 19. On JDK 17 the engine sometimes writes more digits for the
same value, so say so where the text is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kz930

kz930 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

@carloea2 Per Xinyuan's call on the thread, the helper keeps the shortest form and the JDK 17 difference is noted in a comment on _texera_java_text (2b1dcf5). Could you take another look?

The script now raises where the engine's parseField throws, so the spec
asserts that both refuse "not a date" instead of an empty cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kz930

kz930 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

@carloea2 Since your approval I pushed two commits. A timestamp cast now raises on text no format reads, as the engine's parseField does, and the spec checks that. Could you take another look?

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

Retested the timestamp refusal change on fresh upstream main. All 13 TypeCastingOpDescSpec tests and 13 WorkflowToPythonTranslatorSpec tests pass, including invalid text and the input adapter. scalafmtCheckAll passes. Code looks good. The current CI failures occur before tests because the Ampel action is not allowed.

kz930 and others added 10 commits October 23, 2026 14:54
A helper block serves several operators, so a plan pulled in every
function the block defined. CSV File Scan followed by Filter carried
the whole type-cast transcription, about 170 lines the script never
called. The translator now writes the operator code first, follows the
_texera_ calls from it through the helpers, and emits only those
definitions. Anything in a block that is not such a definition is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rted script

The type-cast helpers carried their reasoning as Python comments, so
every exported script that used them printed about 90 lines of notes
written for whoever maintains the export. The notes now live in the
Scaladoc, one line per function, and the Python has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of the exported script

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne shared helper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unding it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cast helpers are written one blank line apart, and the translator joins
helper blocks the same way, so an exported script spaces every helper alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kz930

kz930 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@carloea2 Since your approval, the script carries only the helpers it calls, one blank line apart, and a holed LONG column cast to text no longer rounds values past 2^53 (test in TypeCastingOpDescSpec). Plain ISO timestamps are also read in bulk and a LONG cast clamps inside the cast itself, with the same answers. Could you take another look?

renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Oct 26, 2026
### What changes were proposed in this PR?

The three CSV source operators (CSV, CSV Old and Parallel CSV) implement
`StandaloneCodeGenerator`, so an exported script starts from the same
data the workflow did rather than from a variable the reader has to fill
in. They also carry what every source after them uses: the placeholder a
source writes where its file is named, and the parse that reads a
timestamp column from text.

A source is the one place where the script cannot simply repeat what the
operator does. The engine resolves a dataset through Texera's storage
and hands the operator a URI; a script has no such resolver, so it reads
the file from its own directory. Each source writes a placeholder where
its file should be named and declares the path it reads, and the
translator assigns the names once it can see the whole plan: the last
segment of the path where that is free, and a numbered form of it where
two sources read different files whose paths end the same way. The last
segment rather than a parse of the whole string, because the resolver
percent-encodes the file-relative parts but leaves the repository and
version names as the user typed them, and a dataset version with a space
in its name makes `new URI` throw before any code is generated. That is
what makes an exported script portable, and it is equally its one
precondition: the data has to sit beside the script, under the names the
script states.

A timestamp is read as the engine reads one. The engine hands each field
to DateParserUtils, so a row states its own format, any year reads, an
offset is moved to the local zone, and the reading is cut to the
millisecond. pandas' date parsing does none of that. So each CSV reader
reads the column as text and converts it with the helper apache#8597 adds.

The end of an exported scan window is counted in Long: two Ints the
operator accepts can add up past what an Int holds, and the slice came
out negative.

Two of the changes made in review are deliberately not the literal
instruction, because the three CSV readers do not agree with each other
about a blank field. Old CSV sets `keep_default_na=False` but names no
missing value at all: scala-csv hands a blank back as the empty string
rather than as a null, so the `na_values` the main CSV source needs
would null a value this reader keeps. Parallel CSV asks for such a
column as text rather than as a nullable integer: its schema is read
with scala-csv, where one blank types the whole column as text, while
its executor nulls the blank and parses the rest as that text. Each fix
follows the reader it belongs to rather than the one next to it.

### Any related issues, documentation, discussions?

Part of apache#8325, step 12a; apache#8702 and apache#8703 follow. That issue lists the
set in order. It needs apache#8327 for the trait, so it does not compile until
that lands, and the rows these operators add to the verification runner
follow with the harness rather than as whole new files here. It also
needs apache#8597, whose helpers the timestamp reading calls, so apache#8597 merges
first.

Closes apache#8415, the task this change is the whole of.

One engine defect was found while matching these readers, and it is
fixed here rather than worked around: the export alone could not close
it. apache#8602, where a scan window of no rows left the type inference
nothing to read, so CSV declared a schema of no columns and CSV Old
threw. Its JSONL half follows in apache#8702, which closes it.

### How was this PR tested?

Each operator asserts the block it emits in its own spec. Where a
difference is one of behaviour rather than of text, the spec runs the
engine's own executor and the generated script over the same file and
compares what each produced: the row window the parallel reader ignores,
and the nullable long each reader has to keep exact. Once the
verification lands, every operator is also run both ways on every
configuration its schema offers; this branch is cut from main and does
not carry that machinery, so those runs are not on this diff's CI.

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

Generated-by: Claude Code (Opus 5)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…imestamp helper

A source hands the helper text only, yet the branch for a column that is
already a moment pulled _texera_local_wall_clocks into every exported
script that reads a timestamp column. That branch now lives in
_texera_cast_timestamps, which only Type Casting calls, so a CSV, JSONL
or text source export leaves the helper out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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 generated timestamp helper still differs from the native parser for local times inside daylight-saving gaps. Direct comparisons against DateParserUtils 1.0.11 on JDK 17 give two failures and three passing controls. This is a new timestamp finding, not the accepted floating-point formatting difference.

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

Approving as requested, with the separate timestamp-gap discrepancy still documented in the inline review. This approval does not mark that finding fixed. The JDK 17 floating-point formatting difference was accepted by Xinyuan and is not a blocker.

…ap later, as the engine does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

Please resolve the conflict.

…cast-range

# Conflicts:
#	common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/StandaloneHelpers.scala
#	workflow-compiling-service/src/test/scala/org/apache/texera/amber/translator/WorkflowToPythonTranslatorSpec.scala

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

LGTM!

@aglinxinyuan
aglinxinyuan added this pull request to the merge queue Nov 1, 2026
Merged via the queue into apache:main with commit 711584d Nov 1, 2026
33 checks passed
@kz930
kz930 deleted the fix/8595-timestamp-cast-range branch November 2, 2026 22:08
renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Nov 5, 2026
…ython (apache#8709)

### What changes were proposed in this PR?

Aggregate and the sort family (Sort, Stable Merge Sort and Sort
Partitions) implement `StandaloneCodeGenerator`. The sort family's row
order is its contract, so `LogicalOp` gains `orderSensitive`, which
those three override. Anything comparing two runs of an operator reads
it to decide whether the rows have to arrive in the same order or only
be the same rows.

Review then found five places where the block answered a question the
engine answers differently. Aggregate summed an INTEGER column without
the wrap Java ints have, refused a TIMESTAMP column outright, answered a
timestamp average with a timestamp where AVERAGE is declared DOUBLE, and
spelled a boolean in Python's capitals rather than Java's lower case.
The sort put a NaN last on the way down, where the engine puts it first.

The edge values the shared table now carries found one more. Aggregate's
concatenation wrote a double past 1e7 or under 1e-3, and a timestamp, as
Python prints them, where the engine writes what Java's toString gives.
It now writes every value, the lower-case boolean the review asked for
included, through the helper apache#8597 adds for casting to text.

One change reaches code the engine runs. `getPhysicalPlan` swapped
Aggregate's aggregations for their global-stage form and left them on
the operator. The export compiles a plan and then translates the same
operators, so the script grouped on the result column names and failed.
The operator now keeps what the user set. The desc the global stage
receives is unchanged.

### Any related issues, documentation, discussions?

Part of apache#8325, step 11b; that issue lists the set in order. Aggregate's
concatenation calls a helper apache#8597 adds.

Sorting a NaN apart from a null, and grouping on one, need the nullable
dtypes apache#8704 reads an Arrow file into. Without it a null and a NaN share
a slot and both behave as they did before, so this PR is no worse on its
own and only complete alongside that one.

Closes apache#8707, the task this change is the whole of.

### How was this PR tested?

Each operator asserts the block it emits in its own spec. The Aggregate
cases the review named run in the verify tables of apache#8363, which put the
same input through the engine and through the script and compare the
two. What those tables cannot reach stays here as executed tests: a
timestamp at two resolutions and in a zone other than UTC, and a null
kept apart from a NaN in the sort and the group key.

`AggregateOpDescSpec` builds the physical plan and then checks that the
operator still holds the user's aggregations and that its generated code
still reads the input column.

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

Generated-by: Claude Code (Claude Opus 5)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Nov 5, 2026
…ython (apache#8704)

### What changes were proposed in this PR?

The JSON Lines and Arrow sources implement `StandaloneCodeGenerator`, so
an exported script starts from the same data the workflow did.

Arrow is read into pandas' nullable dtypes rather than its numpy ones.
Arrow says of every value whether it is there, and a numpy column has
nowhere to put that: a missing double and a stored NaN both land on NaN,
and a missing integer costs the column its integer type. The engine
reads the file's own answer, so a set operation keeps a NaN row a null
row does not match, a holed integer column is still made of integers,
and a long past 2^53 is not rounded on its way through a float. The
other sources state the types they need one at a time, their formats
having no way to say it once.

A file also states the width and the sign of each of its numbers, and
Texera has no column narrower than a double or a 32-bit integer. A
single-precision or 16-bit column was arriving null, the parse having no
case for the Float and Short those vectors hand back, and an unsigned
one arrived as the -1 its storage counts down to. Each now reads as the
Texera type that holds it, an unsigned 32-bit column as a long and an
unsigned 64-bit one refused for want of a wider one, and the script
normalizes the same widths so a later sum comes to the same number on
both sides. This is the rule the Parquet source already follows. The end
of an exported Arrow window is counted in Long, as the CSV readers count
theirs.

A timestamp is read as the engine reads one. The engine hands each field
to DateParserUtils, so a row states its own format, any year reads, an
offset is moved to the local zone, and the reading is cut to the
millisecond. pandas' date parsing does none of that. So JSONL reads the
column as text and converts it with the helper apache#8597 adds.

This PR also changes the engine. `ArrowUtils` handed a TIMESTAMP column
to a Python worker as an Arrow millisecond timestamp labelled UTC. A
Python operator saw an aware datetime cut to the millisecond, and a year
past 2262 overflowed. It is now a zoneless microsecond timestamp. That
is what Texera's own Python side declares for a TIMESTAMP
(`pa.timestamp("us")`), and what an exported script holds. Two things a
user can see change. A Python UDF now receives a naive datetime instead
of one in UTC. A result downloaded as an Arrow file now carries a
zoneless microsecond timestamp column instead of a UTC millisecond one.
A TIMESTAMP parameter a Python UDF declares is now parsed the same way,
as a naive wall clock with any offset moved to the machine's zone, so it
compares with a row's instead of raising.

### Any related issues, documentation, discussions?

Part of apache#8325, step 12b; that issue lists the set in order. It uses
apache#8341's shared source code and apache#8597's timestamp helpers.

Its JSON Lines export reads a JSON `null` as the text `"null"`, as the
engine does today (apache#8674). If apache#8740 merges first, that part changes to
read a null before this one merges.

Closes apache#8702, the task this change is the whole of.

It also finishes apache#8602, where a scan window of no rows left the type
inference nothing to read and JSONL declared a schema of no columns.
apache#8341 fixes the CSV half.

Closes apache#8602.

### How was this PR tested?

Each operator asserts the block it emits in its own spec, and the Arrow
spec runs the engine's own executor and the generated script over the
same file. The engine change is covered in both ArrowUtilsSpec files,
which include a round trip on either side of the nanosecond range, and
in the Python tests of the timestamp parser. Once the verification
lands, every operator is also run both ways on every configuration its
schema offers; this branch is cut from main and does not carry that
machinery, so those runs are not on this diff's CI.

Arrow is the one worth calling out. Its column of timestamps only
matches once apache#7672 is in: before that the engine read the file's numbers
through the JVM's zone while `pd.read_feather` read the wall clock the
file states, and the two differed by whatever the machine was set to.
That change has since landed.

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

Generated-by: Claude Code (Opus 5)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Nov 5, 2026
…as Python (apache#8705)

### What changes were proposed in this PR?

File Scan, the file-scan source, Text Input and URL Fetcher implement
`StandaloneCodeGenerator`. Each slices the raw lines before parsing, as
the engine does, so a line outside the window is never converted. A
boolean line is read as `parseField` reads one, where 1 is true and
other text ends the run. A timestamp line is kept as text and parsed as
a column with the helper apache#8597 adds, as the CSV readers parse theirs.
Every file is read as bytes and decoded the way the engine's reader
decodes it. Bytes the charset cannot hold become U+FFFD, and UTF-16 with
no byte-order mark reads as big-endian. Python's codecs refuse both, so
a file read under the wrong Encoding used to end the script while the
engine read on.

URL Fetcher is reported as unverifiable rather than exported blind. It
reads a live URL, so two runs are not required to agree and a comparison
would only measure the network. File Scan takes its filenames from an
input port, so the verification feeds it a table that names the files to
read, as it feeds a transform. That lands with apache#8364 and apache#8363.

### Any related issues, documentation, discussions?

Part of apache#8325, step 12c; that issue lists the set in order. It needs
apache#8341 for the shared source code and apache#8597 for the timestamp helpers, so
both merge first.

Closes apache#8703, the task this change is the whole of.

One engine defect was found while matching these readers, and it is
fixed here rather than worked around: the export alone could not close
it. apache#8598, where a file scan extracting an archive could not also
include the filename, because the line-by-line branch emitted a
one-field row against the two-column schema its own flag declares. A
second, apache#8596, where the Encoding field never reached the executor, was
fixed upstream by apache#8605, and this branch takes that fix.

Closes apache#8598.

### How was this PR tested?

Each operator asserts the block it emits in its own spec. Where a
difference is one of behaviour rather than of text, the spec runs the
engine's own executor and the generated script over the same input and
compares what each produced: the boolean parsing rules and the entries
read out of an archive. Once the verification lands, every operator is
also run both ways on every configuration its schema offers; this branch
is cut from main and does not carry that machinery, so those runs are
not on this diff's CI.

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

Generated-by: Claude Code (Opus 5)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Nov 5, 2026
… the engine does (apache#8980)

### What changes were proposed in this PR?

Filter's exported Python compared the column to the value with pandas
operators. The engine's `FilterPredicate.evaluate` compares by the
column's type instead, so the two disagreed on numbers stored as text,
booleans, NaN and -0.0.

Each comparison in the export now goes through `_texera_filter_keep`, a
transcription of `FilterPredicate.evaluate`:

| Column type | Comparison |
|---|---|
| STRING | as Java doubles when both sides parse as one, otherwise by
UTF-16 text |
| BOOLEAN | the lower-case text "true"/"false" |
| INTEGER, DOUBLE | by `Double.compareTo`: NaN equals NaN and sorts
above every number, -0.0 sorts below 0.0; a hex value past the largest
double is infinity, as in Java |
| LONG | as integers, rejecting what `Long.valueOf` rejects |
| TIMESTAMP | by whole milliseconds; a value with an offset is read as
the wall clock it shows in the machine's zone, with the offset that zone
had at that instant |

The column type comes from the input schema. Without one, it is inferred
from the pandas dtype. `is null` and `is not null` are unchanged.

One approximation remains. The engine reads a TIMESTAMP value with
DateParserUtils, which accepts more formats than pandas. A value pandas
cannot read raises an error, so the export never gives a silently wrong
answer.

A value with an offset is placed in the machine's zone by
`_texera_local_wall_clock` from apache#8597. It uses the zone's IANA history,
as the JVM does, and not Windows' localtime. Before 1900 it takes the
zone's standard offset, as the engine does.

### Any related issues, documentation, discussions?

Closes apache#8977

Depends on apache#8597, which adds `_texera_local_wall_clock`, and on apache#9035,
which reads a column named in a different case.

### How was this PR tested?

A new test in `SpecializedFilterOpDescSpec` runs 33 predicates across
the six column types through `SpecializedFilterOpExec` and through the
export, and compares the kept rows by input position. A second test does
the same for timestamp values with an offset, with both the JVM and
Python set to Asia/Kolkata. Both fail on main and pass with this change.
A third test sets both to America/Mexico_City and compares
`2021-07-01T00:00:00Z` (summer time) and `1931-01-01T00:00:00Z` (an
older offset). With apache#8597 merged in, the Filter specs pass (42 tests). A
fourth test compares a LONG column `Key` through a predicate on `KEY`,
past 2^53, and passes with apache#8597 and apache#9035 merged in.

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

Generated-by: Claude Code (Claude Opus 5.5)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

common fix platform Non-amber Scala service paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Casting text to a timestamp empties a moment the engine reads

4 participants