Repository navigation
feat(operator): read a timestamp cast over the years the engine reaches - #8597
Conversation
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>
Automated Reviewer SuggestionsBased on the
|
|
| 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 Report❌ Patch coverage is 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
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
carloea2
left a comment
There was a problem hiding this comment.
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>
|
Fixed in |
carloea2
left a comment
There was a problem hiding this comment.
The timezone crash is fixed. I ran the current helper with Z, an explicit offset, year 2500, and null. Those checks passed.
carloea2
left a comment
There was a problem hiding this comment.
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>
… write text as Java's toString does Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@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
left a comment
There was a problem hiding this comment.
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.
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>
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>
|
@carloea2 Since your approval I pushed two commits. A timestamp cast now raises on text no format reads, as the engine's |
carloea2
left a comment
There was a problem hiding this comment.
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.
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>
|
@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? |
### 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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
…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>
…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>
…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>
… 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>
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, where2500-01-01 00:00:00is an ordinary moment: it reads it, and the exported script answered with an empty cell. The same for1500-06-15 08:30:00and9999-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
DateParserUtilsone field at a time, so a row states its own format. A column holding2024-03-05 14:09:07andMarch 5, 2024kept 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
parseFieldends 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:
Zor+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.parseFieldreturns ajava.sql.Timestampuntouched, so the script now hands such a column back at the resolution it arrived in.DateParserUtilsreads into ajava.util.Date, which counts milliseconds, so the script now cuts there too.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_textnow writes each one as Java'stoStringdoes.NaNas text, 0 as an integer andtrueas a boolean. The casts now read empty cells from the column's own mask, and the double cast builds a nullableFloat64, so a NaN read from text stays one through the next cast. A double past an integer's bounds now saturates astoIntandtoLongdo, whereint()raised on an infinity.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 asjava.util.Datedoes.localtimerefuses an instant before 1970, which stopped the cast. The same helper now falls back to the zone's standard offset there.That
Float64column changed what a Python operator after this cast is handed. pyamber hands a Python operator a DOUBLE column asfloat64, where a NaN and a null are one value, and writes a NaN out as null. GivenFloat64instead, Sort ordered a NaN apart from the nulls, a chart drew points the engine dropped, and a passed-through NaN stayed NaN.StandaloneCodeGeneratorgainsstandaloneInputAdapter(), and for a Python operator the translator feeds the fragment afloat64copy 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
TypeCastingOpDescSpecrun the generated Python and compare it cell by cell againstAttributeTypeUtils.parseField. They are tagged to run in the integration job, which has pandas.2500-01-01,1500-06-15,9999-12-31, readings withZand+05:30, and readings past the millisecond. It fails on the old generator.AttributeTypeUtils, and check that a NaN read from text survives a second cast.America/Mexico_Cityin 1899, 1921, 1931 and 2021, at the offset the zone had then, and the same moments withlocaltimerefusing those before 1970.WorkflowToPythonTranslatorSpeccheck 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)