raminqaf opened a new pull request, #29370:
URL: https://github.com/apache/flink/pull/29370

   ## What is the purpose of the change
   
   `CAST(s AS VARIANT)` decoded the UTF-8 bytes of `s` into a Java `String` and 
then encoded the `String` back to the same bytes. `CAST(d AS VARIANT)` built a 
`BigDecimal` from a compact decimal only for the builder to take it apart 
again. This PR copies the string bytes and writes the unscaled long directly. 
The stored `VARIANT` does not change:
   
   | Input                       | Stored before and after                      
|
   
|-----------------------------|----------------------------------------------|
   | `47 72 C3 BC C3 9F 65`      | `47 72 C3 BC C3 9F 65`, the string `Grüße`   
|
   | `61 FF 62`                  | `61 EF BF BD 62`, so `FF` becomes U+FFFD     
|
   | `1.50` as `DECIMAL(10, 2)`  | decimal4 with unscaled value 150 and scale 2 
|
   
   A `BinaryStringData` can hold invalid UTF-8, but the Variant spec requires 
valid strings. The decode used to hide this by replacing every malformed 
sequence with U+FFFD. The new path checks the bytes in one pass with 
`StringUtf8Utils#firstInvalidUtf8ByteIndex`, which applies the same checks as 
the strict decoder. Invalid bytes still go through the decode, so they are 
stored exactly as before. Rejecting invalid UTF-8 instead would make every 
string cast to `VARIANT` fallible, so I left it for a separate decision.
   
   JMH, JDK 17, 3 to 5 forks. Each call reads a fresh `BinaryStringData` or 
`DecimalData` from a `MemorySegment` at an offset, like `BinaryRowData` does.
   
   | Cast to `VARIANT`        | Size   | Before  | After   | Change |
   |--------------------------|--------|---------|---------|--------|
   | `STRING`, ASCII          | 16 B   | 31.6 ns | 23.0 ns | -27%   |
   | `STRING`, ASCII          | 1 KiB  | 325 ns  | 227 ns  | -30%   |
   | `STRING`, ASCII          | 64 KiB | 21.0 µs | 11.1 µs | -47%   |
   | `STRING`, non-ASCII      | 16 B   | 37.3 ns | 25.5 ns | -32%   |
   | `STRING`, non-ASCII      | 1 KiB  | 1.33 µs | 0.87 µs | -35%   |
   | `STRING`, non-ASCII      | 64 KiB | 84.8 µs | 47.8 µs | -44%   |
   | `STRING`, invalid UTF-8  | 1 KiB  | 1.89 µs | 1.84 µs | -2%    |
   | `DECIMAL(9, 2)`          |        | 25.7 ns | 15.4 ns | -40%   |
   | `DECIMAL(18, 2)`         |        | 27.4 ns | 15.5 ns | -44%   |
   | `DECIMAL(38, 2)`         |        | 39.6 ns | 36.8 ns | -7%    |
   
   A 64 KiB string allocates 263 KB per call instead of 459 KB for ASCII and 
623 KB for non-ASCII. The invalid UTF-8 row puts the invalid byte last, so the 
check reads the whole string before it falls back. `DECIMAL(38, 2)` is not 
compact and takes the unchanged `BigDecimal` path, so its difference is noise.
   
   ## Brief change log
   
     - `BinaryVariantInternalBuilder#appendString(byte[])` copies UTF-8 bytes 
as they are. `appendString(String)` encodes and delegates to it.
     - `BinaryVariantInternalBuilder#appendDecimal(long, int)` writes a 
decimal4 or a decimal8 from the unscaled value and picks the width like 
`appendDecimal(BigDecimal)`. Anything wider, or with a negative scale, goes 
through `appendDecimal(BigDecimal)`.
     - `VariantCastUtils#fromString` and `#fromDecimal` use the two new methods.
   
   ## Verifying this change
   
   This change added tests and can be verified as follows:
   
     - `BinaryVariantInternalBuilderTest`: `appendString(byte[])` stores the 
bytes as they are under a short and a long string header. `appendDecimal(long, 
int)` produces the same bytes as `appendDecimal(BigDecimal)` and the expected 
width at 10^9, 10^18, scales 9, 10, 18 and 19, `Long.MIN_VALUE`, and a negative 
scale.
     - `VariantCastUtilsTest`: a string read from a segment offset stores its 
UTF-8 bytes. Four invalid inputs store the same U+FFFD value as before: a stray 
`FF`, a truncated `C3`, the overlong `C0 AF`, and the surrogate `ED A0 80`. 
Compact and non-compact decimals at precisions 9, 10, 18, 19 and 38 match the 
`BigDecimal` result.
     - These tests fail if the UTF-8 check is skipped or if the decimal4 bound 
is off by one.
     - Existing `CastRulesTest` and `CastFunctionITCase` pass.
   
   ## Does this pull request potentially affect one of the following parts:
   
     - Dependencies (does it add or upgrade a dependency): no
     - The public API, i.e., is any changed class annotated with 
`@Public(Evolving)`: no
     - The serializers: no
     - The runtime per-record code paths (performance sensitive): yes. Casting 
a `STRING` or a `DECIMAL` to `VARIANT` runs per record. The benchmark above 
covers both.
     - Anything that affects deployment or recovery: JobManager (and its 
components), Checkpointing, Kubernetes/Yarn, ZooKeeper: no
     - The S3 file system connector: no
   
   ## Documentation
   
     - Does this pull request introduce a new feature? no
     - If yes, how is the feature documented? not applicable
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes (please specify the tool below)
   
   Generated-by: Claude Opus 5.5
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to