[FLINK-40825][table] Support casting primitive types to VARIANT - #29311
Conversation
dec1946 to
1345a2e
Compare
e4f57af to
312aa6e
Compare
AHeise
left a comment
There was a problem hiding this comment.
Thanks, the direction looks good, and keeping the kind of the SQL type is the right call. Two things before merge: the size limit makes string and binary casts fail even though the rule says they can't, and ARRAY/ROW/MAP of VARIANT are now castable, which is untested and undocumented. The rest are questions and nits.
…NT to FLOAT or DOUBLE Casting a VARIANT to FLOAT or DOUBLE rejected every non-finite result as an overflow, so a VARIANT holding NaN or infinity, for example one read by the Avro converter, could not be read back. The FLOAT cast now rejects only a finite value that does not fit, such as 1e40, and keeps a stored NaN or infinity. The DOUBLE cast needs no check, since every numeric kind fits a double.
312aa6e to
1759486
Compare
AHeise
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround, everything from the first round is addressed. One open question about VARIANT as a MAP key or MULTISET element; the rest are nits. Please squash the two follow-up commits into "Support casting primitive types to VARIANT" before merge, keeping the NaN commit separate. "Cover casts … already worked" describes the review history rather than the change.
CAST and TRY_CAST now convert BOOLEAN, numeric, character string, binary string, DATE, TIME, TIMESTAMP, TIMESTAMP_LTZ, and UUID values to VARIANT. SqlCastFunction rejected every cast to VARIANT, and now routes a VARIANT target to LogicalTypeCasts, which lists the supported sources explicitly. A type without a VARIANT kind, such as INTERVAL, is rejected at validation, and the cast is explicit only. The value keeps the kind of its SQL type, so an integer keeps its width and a string is wrapped rather than parsed. NaN and infinity are stored as is, since a VARIANT is not limited to what JSON can express. A timestamp keeps its declared precision: up to 6 it is stored with microseconds and above with nanoseconds. Nanoseconds only cover 1677-09-21 to 2262-04-11, so such a value outside that range fails the cast. A VARIANT holds at most 16 MiB, so a longer string or binary value fails the cast too. The rule reports a type as fallible when its precision or declared length allows such a value, so TRY_CAST returns NULL for it. A constructed cast checks each child with the same rules, so a cast such as ARRAY<INT> to ARRAY<VARIANT> works element by element. Casting a whole ARRAY, MAP, or ROW into one VARIANT follows in FLINK-40826.
1759486 to
9e51eb8
Compare
What is the purpose of the change
FLIP-521 lists the types that cast to and from VARIANT. The direction VARIANT to SQL type exists, but
SqlCastFunctionrejected every cast to VARIANT. This PR addsCASTandTRY_CASTfrom primitive types to VARIANT. Casting a whole ARRAY, MAP, or ROW into one VARIANT follows in FLINK-40826.A type casts to VARIANT only if a VARIANT kind holds its value without loss. Any other type is rejected at validation.
CAST(CAST(1 AS BIGINT) AS VARIANT)1, stored as BIGINTCAST(42 AS VARIANT)42, stored as INTCAST('{"a": 1}' AS VARIANT)'{"a": 1}', not an objectCAST(CAST(NULL AS INT) AS VARIANT)NULL, not a variant nullCAST(CAST('NaN' AS DOUBLE) AS VARIANT)NaN, stored as DOUBLECAST(ts AS VARIANT)forts TIMESTAMP(9)in the year 3000TRY_CAST(REPEAT('x', 17000000) AS VARIANT)NULL, a VARIANT holds at most 16 MiBCAST(ARRAY[1, NULL] AS ARRAY<VARIANT>)[1, NULL], each element a VARIANT, theNULLstays SQLNULLCAST(INTERVAL '2' DAY AS VARIANT)CAST(ARRAY[1, 2] AS VARIANT)Brief change log
1e40. Before, a VARIANT holding NaN, for example from the Avro converter, could not be read back. This is the first commit.SqlCastFunction#canCastFromroutes a VARIANT target toLogicalTypeCastsinstead of rejecting it, the same way it handles UUID.LogicalTypeCastsdeclares the supported sources explicitly: BOOLEAN, the numeric types, CHAR, VARCHAR, BINARY, VARBINARY, DATE, TIME, TIMESTAMP, TIMESTAMP_LTZ, and UUID. The cast is explicit only.PrimitiveToVariantCastRule, backed byVariantCastUtils#fromXxxhelpers. It fails only for aTIMESTAMP(p)orTIMESTAMP_LTZ(p)withpabove 6 whose value is outside the nanosecond range, and for a string or binary value over 16 MiB.canFailreports exactly the types whose precision or declared length allows such a value, soTRY_CASTreturnsNULLfor it.data-types.md, the VARIANT column of the cast matrix, and TIME and UUID in the list of VARIANT kinds.ARRAY<INT>toARRAY<VARIANT>, and the same for ROW and MAP, works element by element at any depth. ANULLelement stays a SQLNULL. A whole ARRAY, MAP, or ROW into one VARIANT is still rejected and follows in FLINK-40826. This also allows aMAP<VARIANT, ...>key and aMULTISET<VARIANT>element. Two VARIANT values are equal only when their bytes match, so a key cast from INT does not match the same number cast from BIGINT. The docs say so.Notes for reviewers:
PARSE_JSON('1')picks the smallest kind because JSON text carries no width, but a cast keeps the declared type. Both are valid under the Parquet Variant spec, which treats all integer widths as one equivalence class.PARSE_JSONstays the way to parse JSON text.PARSE_JSONrejects them only because JSON has no literal for them.JSON_STRINGstill fails on such a value. Printing andCAST(v AS STRING)showNaN. The json and raw formats calltoJson()as well, so writing such a value to those sinks fails the job. The docs say so.CAST(TIMESTAMP(p) AS STRING)prints exactlypdigits and the Avro format picks millis or micros fromp. Up to a precision of 6 it is stored with microseconds, above with nanoseconds, even for a value without digits below a microsecond. Nanoseconds only cover 1677-09-21 to 2262-04-11, so for a precision above 6 a value outside that range fails, andTRY_CASTreturnsNULL.VariantBuilderpicks the kind by value, so the helpers useBinaryVariantInternalBuilderdirectly.TIMEis stored in microseconds, the only TIME precision of the VARIANT spec. Every FlinkTIMEfits, since the runtime keeps milliseconds.canFailtrusts the declared length of a string or binary type. Flink does not enforce that length on values from a source, soTRY_CASTwould still fail for a longer value in aVARCHAR(100). That is pathological, and the Javadoc says so.SqlCastFunctionis a copy of the Calcite class. The change replaces its existing TODO to support casts to VARIANT.LogicalTypeCastsTestrow asserted that UUID does not cast to VARIANT. It now asserts that it does.Verifying this change
This change added tests and can be verified as follows:
LogicalTypeCastsTestcovers every supported source, and rejects INTERVAL, TIMESTAMP WITH TIME ZONE, MULTISET, ARRAY, MAP, and ROW. It allowsARRAY<INT>toARRAY<VARIANT>, ROW and MAP alike, and rejectsARRAY<INTERVAL>toARRAY<VARIANT>.VariantCastUtilsTestpins the size limit: 16,777,211 bytes cast, one more byte fails with a clear message.CastRuleProviderTestchecks rule resolution, including the array, row, and map rules for constructed targets, that VARIANT to VARIANT stays the identity, and which types can fail: a timestamp with a precision above 6, and a string or binary type whose declared length allows more than 16 MiB, with the exact boundaries.CastRulesTestchecks the stored kind for every source, including the kept integer width,TIME(0)andTIME(3)up to the last millisecond of the day,TIMESTAMP(3),TIMESTAMP(6), and aTIMESTAMP(9)without digits below a microsecond, NaN and infinity, and SQL NULL. It checks that aTIMESTAMP(9)andTIMESTAMP_LTZ(7)outside the nanosecond range fail, while aTIMESTAMP(6)in the year 3000 casts. It also covers NaN and infinity read back from VARIANT to FLOAT and DOUBLE. It covers a pre-epoch timestamp and element-wise casts intoARRAY<VARIANT>,ROW<.. VARIANT>, andMAP<STRING, VARIANT>, and intoMAP<VARIANT, STRING>keys andMULTISET<VARIANT>elements.CastFunctionITCaseround-trips each type through VARIANT in SQL and the Table API, for literals and for values computed at runtime, includingTIME, NaN, and infinity. It checks that a lateTIMESTAMP(9)fails withCAST, returnsNULLwithTRY_CAST, and that a lateTIMESTAMP(6)round-trips. It also checks the validation errors. It round-trips TINYINT, SMALLINT,CHAR(n)with its padding,BINARY(n)with its zero padding, and the constructed casts. It checks thatTRY_CASTreturnsNULLfor a string over 16 MiB, and that aMAP<VARIANT, STRING>key cast from INT matches an INT VARIANT but not a BIGINT one.Does this pull request potentially affect one of the following parts:
@Public(Evolving): noDocumentation
data-types.md(English and Chinese)Was generative AI tooling used to co-author this PR?
Generated-by: Opus 5.5