[FLINK-37925][table] Define VARIANT cast rules so a cast never alters the stored value - #28758
Open
raminqaf wants to merge 2 commits into
Open
[FLINK-37925][table] Define VARIANT cast rules so a cast never alters the stored value#28758raminqaf wants to merge 2 commits into
raminqaf wants to merge 2 commits into
Conversation
raminqaf
force-pushed
the
FLINK-37925-followup
branch
2 times, most recently
from
July 16, 2026 10:07
beec0e0 to
e03672b
Compare
raminqaf
commented
Jul 16, 2026
raminqaf
force-pushed
the
FLINK-37925-followup
branch
2 times, most recently
from
July 16, 2026 10:45
f02dbdc to
49d4bca
Compare
raminqaf
force-pushed
the
FLINK-37925-followup
branch
6 times, most recently
from
July 17, 2026 11:38
3750b25 to
f9bd3a0
Compare
snuyanzin
reviewed
Jul 17, 2026
snuyanzin
reviewed
Jul 17, 2026
snuyanzin
reviewed
Jul 20, 2026
snuyanzin
reviewed
Jul 20, 2026
davidradl
reviewed
Jul 20, 2026
davidradl
reviewed
Jul 20, 2026
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
July 21, 2026 09:41
02f5ffc to
e9c07c4
Compare
twalthr
reviewed
Jul 21, 2026
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
July 22, 2026 11:00
0a0d8d0 to
8f09c44
Compare
twalthr
reviewed
Jul 22, 2026
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
July 22, 2026 16:31
4dc9135 to
23e41c6
Compare
twalthr
reviewed
Jul 23, 2026
twalthr
left a comment
Contributor
There was a problem hiding this comment.
Another round of comments.
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
July 24, 2026 10:55
23e41c6 to
ac56197
Compare
snuyanzin
reviewed
Jul 27, 2026
snuyanzin
reviewed
Jul 27, 2026
snuyanzin
reviewed
Jul 27, 2026
snuyanzin
reviewed
Jul 27, 2026
raminqaf
force-pushed
the
FLINK-37925-followup
branch
3 times, most recently
from
July 29, 2026 15:48
c0cc0b7 to
3a11673
Compare
raminqaf
force-pushed
the
FLINK-37925-followup
branch
2 times, most recently
from
July 29, 2026 15:55
30006cb to
1da4431
Compare
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
July 30, 2026 12:24
1da4431 to
55b0abc
Compare
… the stored value A cast from a VARIANT succeeds only when the target holds the stored value without altering it, so a value is never wrapped, rounded, truncated, or padded to make it fit. Anything else fails `CAST` and returns `NULL` for `TRY_CAST`. An integer converts to any integer target that has room for it, and to a `DECIMAL` that holds it exactly. A `DECIMAL` converts to a `DECIMAL` whose precision and scale keep every digit, where trailing zeros may be appended but a scale that would round is rejected. A timestamp converts to a `TIMESTAMP` whose precision keeps its fractional seconds, which matters because a variant always stores microseconds. `CHAR` and `BINARY` require the exact length while `VARCHAR` and `VARBINARY` accept anything shorter. `FLOAT` and `DOUBLE` are the exception. They are approximate by definition, so they accept every numeric kind and drop decimal digits, rejecting only a magnitude they cannot represent at all. Reading one kind as another is never implicit. A `DECIMAL` is not read as an integer, and a `TIMESTAMP` is not read as a `TIMESTAMP_LTZ`. To reach such a type, the inner cast names the stored kind and a regular cast around it performs the conversion, which then applies the usual rules and may round, truncate, or overflow. Casting to a character string extracts the scalar value, so a stored string is returned unquoted. Previously this reused the JSON serialization and was indistinguishable from `JSON_STRING`, which stays the way to obtain the JSON text. A variant holding an object, array, or binary value is not castable to a character string, and a variant storing a JSON `null` casts to SQL `NULL`. `TIME` has no counterpart in the variant type model and is rejected during validation.
Describe which kind a `VARIANT` stores for each JSON input, since `PARSE_JSON` picks the smallest integer kind that holds a value and stores a plain-notation number as a `DECIMAL` but a scientific-notation one as a `DOUBLE`. Note that `NaN` and infinity are not valid JSON, and show how to keep such a value as a string instead. Add a table of the cast targets each stored kind reaches, with the range, precision, scale, and length conditions that apply, and the pattern for reaching a type the table does not list. Fix the `Variant.getInstant` javadoc to reference `Type.TIMESTAMP_LTZ` rather than `Type.TIMESTAMP`, document that a timestamp is stored with microsecond precision, and link the referenced types.
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
July 30, 2026 12:51
55b0abc to
83b53d4
Compare
twalthr
reviewed
Jul 30, 2026
Comment on lines
+1587
to
+1591
| .fail(VARIANT(), Variant.newBuilder().of(7.0d), TableRuntimeException.class) | ||
| .fail( | ||
| VARIANT(), | ||
| Variant.newBuilder().of(new BigDecimal("7.0")), | ||
| TableRuntimeException.class) |
Contributor
There was a problem hiding this comment.
This one we could rediscuss, right?
Comment on lines
+263
to
+272
| .testResult( | ||
| call("PARSE_JSON", "123.456").cast(STRING()), | ||
| "CAST(PARSE_JSON('123.456') AS STRING)", | ||
| "123.456", | ||
| STRING().notNull()) | ||
| .testResult( | ||
| call("PARSE_JSON", "true").cast(STRING()), | ||
| "CAST(PARSE_JSON('true') AS STRING)", | ||
| "true", | ||
| STRING().notNull()) |
Contributor
There was a problem hiding this comment.
This we did not discuss. Let's not allow this for now. Only string can be cast to string.
| * scale. | ||
| */ | ||
| public static DecimalData toDecimal(Variant variant, int precision, int scale) { | ||
| final String targetType = String.format("DECIMAL(%d, %d)", precision, scale); |
Contributor
There was a problem hiding this comment.
only format on error, keep the hot path performant
| * VARCHAR} an upper bound). | ||
| */ | ||
| public static String toStringValue(Variant variant, int targetLength, boolean charTarget) { | ||
| final String targetType = |
| default: | ||
| // Scalars are cast to their raw string value. | ||
| } | ||
| final String value = variant.get().toString(); |
Contributor
There was a problem hiding this comment.
this is again JSON and not SQL. e.g. TRUE vs true for variant boolean to string. as mentioned above. only string variant can cast to string. if you want to support more, come up with a proper string representation for each variant type. incl TS_LTZ in users time zone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the purpose of the change
Follow-up to the initial
VARIANTto primitive cast support in FLINK-37925. It settles the cast semantics under one rule and turns the string cast into a real value cast.A cast from a
VARIANTsucceeds only when the target holds the stored value without altering it. A value is never wrapped, rounded, truncated, or padded to make it fit; anything else failsCASTand returnsNULLforTRY_CAST.DECIMAL,FLOAT,DOUBLEFLOAT,DOUBLEFLOAT,DOUBLEDECIMALDECIMALwithout rounding,FLOAT,DOUBLEBOOLEAN,DATETIMESTAMPTIMESTAMP(p)that keeps the fractional secondsTIMESTAMP_LTZTIMESTAMP_LTZ(p)that keeps the fractional secondsBYTESBINARY(n),VARBINARY(n)CHAR(n),VARCHAR(n),STRINGNULLNULLfor any nullable targetThe conditions in that table are:
CAST(PARSE_JSON('1000') AS TINYINT)fails instead of wrapping.DECIMALtarget has to fit the precision and the scale. Trailing zeros may be appended, so42reachesDECIMAL(5, 2)as42.00, but a scale that would round is rejected.FLOATandDOUBLEare the deliberate exception. They are approximate by definition, so they accept every numeric kind and drop decimal digits, rejecting only a magnitude they cannot represent, such as1e40to a
FLOAT..123reachesTIMESTAMP(3)while.123456does not.CHAR(n)andBINARY(n)require the exact length,VARCHAR(n)andVARBINARY(n)at mostn. Nothing is padded or truncated.Reading one kind as another is never implicit, so a
DECIMALis not read as an integer and aTIMESTAMPis not read as aTIMESTAMP_LTZ. To reach such a type the inner cast names the stored kind and a regular cast around it does the conversion, which then applies the usual rules and may round, truncate, or overflow:CAST(VARIANT AS CHAR/VARCHAR)now extracts the scalar value, so a stored string is returned unquoted asfoo. Previously it reused the JSON serialization and was indistinguishable fromJSON_STRING, whichremains the way to obtain the JSON text where a string stays quoted as
"foo". A variant holding an object, array, or binary value is not castable to a character string, and a variant storing a JSONnullcaststo SQL
NULLrather than to the textnull.Brief change log
VariantCastUtilsin flink-table-runtime holding the per-target checks: integer range,DECIMALprecision and scale, floating-point magnitude, timestamp precision, and character or binary length.VariantToPrimitiveCastRule: route every target through those checks. This also fixes two cases that silently altered the value, namely a timestamp keeping more precision than its declared type and a binaryvalue being padded or truncated to the target length.
VariantToStringCastRule: extract the scalar value instead of serializing to JSON, handleCHARandVARCHARdirectly to enforce the length, and map a null-valued variant to SQLNULLfor a nullable target.LogicalTypeCasts: expressVARIANTcast validation in the per-target rules and drop theJSON_STRINGcast hint.Verifying this change
This change added and updated tests:
CastRulesTest: per-kind coverage of every numeric target including range rejection,DECIMALprecision and scale fitting, theFLOATandDOUBLEacceptance of any numeric kind with overflow rejection, the timestamp precision rule, and theTIMESTAMPversusTIMESTAMP_LTZsplit.CastFunctionITCase: the same throughPARSE_JSON, plus string value extraction, strictCHARandVARCHARlength, rejection of object and array values, a JSONnullcasting to SQLNULL, and the two nested casts above. Each failing case also asserts thatTRY_CASTreturnsNULL.LogicalTypeCastsTest: theVARIANTcast support matrix.Trade-offs
Rejecting a cast that would change the value is stricter than Snowflake and Spark, which coerce. For the kind-preserving part it matches Snowflake's
AS_INTEGERandAS_DOUBLEaccessor family, where a mismatch yieldsNULL. The reasoning is that aVARIANTcast is an extraction step, so any conversion decision belongs in an explicit second cast, andTRY_CASTprovides the non-failing form.Because the stored kind is only known per row, these are runtime failures and cannot be reported during validation.
Does this pull request potentially affect one of the following parts:
@Public(Evolving): yes, javadoc-only change to the@PublicEvolvingVariantinterface, no signature changeVARIANTcast pathDocumentation