[FLINK-37925][table] Define VARIANT cast rules so a cast never alters the stored value - #28758
Open
raminqaf wants to merge 3 commits into
Open
[FLINK-37925][table] Define VARIANT cast rules so a cast never alters the stored value#28758raminqaf wants to merge 3 commits into
raminqaf wants to merge 3 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
2 times, most recently
from
July 30, 2026 12:51
55b0abc to
83b53d4
Compare
twalthr
reviewed
Jul 30, 2026
raminqaf
force-pushed
the
FLINK-37925-followup
branch
4 times, most recently
from
July 31, 2026 12:38
cb0c8be to
0c314b5
Compare
twalthr
reviewed
Jul 31, 2026
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
August 3, 2026 09:02
0c314b5 to
709ec03
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.
Result display builds a cast to `STRING` for every column, so restricting the VARIANT cast to scalar values also made a variant holding an object or an array undisplayable. `SELECT PARSE_JSON('[1,"two",false,null]')` failed in `TableResult#print`, in the SQL client, and in the SQL gateway, which covers the main use case of the type.
Render every variant as JSON when the cast context is printing. `CastRule.Context` already carries that flag and `BinaryToStringCastRule` already branches on it, so a display path keeps working while `CAST` stays strict. This restores what a result looked like before the cast rules were defined, including a stored string printing quoted.
Handling this in the cast rule rather than in `RowDataToStringConverterImpl` also covers a variant nested in an `ARRAY`, `MAP`, or `ROW` column, because the printing flag propagates to the child rules that render the elements.
raminqaf
force-pushed
the
FLINK-37925-followup
branch
from
August 4, 2026 11:49
488f31f to
c110934
Compare
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:
PARSE_JSON('7.0')reachesINTas7whilePARSE_JSON('7.2')does not, andCAST(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 have to round is rejected.FLOATandDOUBLEare the one exception to exactness. They are approximate by definition, so they accept every numeric kind and drop decimal digits, rejecting only a magnitude they cannot represent such as1e40to aFLOAT.CHAR(n)requires the exact length andVARCHAR(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:Casting to a character string
CAST(VARIANT AS CHAR/VARCHAR)now renders the value exactly as a regular SQL cast of the stored kind would, instead of reusing the JSON serialization. So a boolean becomesTRUErather thantrue, a timestamp uses the SQL format2021-09-24 12:34:56.123456rather than the ISO form with aT, and aTIMESTAMP_LTZis shifted into the session time zone rather than always printed as UTC. This is implemented by calling the same runtime formatters the native to-string rules call, so the two cannot drift apart, and the tests assert against the very constants the native cases use.JSON_STRINGremains the way to obtain the JSON text, where a string stays quoted as"foo"and an object or array is serialized. A variant holding an object, array, or binary value has no scalar rendering, so that cast fails and the error points toJSON_STRING. A variant storing a JSONnullcasts to SQLNULLrather than to the textnull.Kinds that PARSE_JSON cannot produce
The same rule applies to the kinds a variant can hold but
PARSE_JSONnever creates. A timestamp reaches aTIMESTAMPwhose precision keeps its fractional seconds, since a variant always stores microseconds, and a binary value reachesBINARY(n)of the exact length orVARBINARY(n)at mostn.TIMESTAMPandTIMESTAMP_LTZare separate kinds and are not read as one another. These paths are only reachable through the programmaticVariantBuildertoday.TIMEhas no counterpart in the variant type model and is rejected during validation.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