-
Notifications
You must be signed in to change notification settings - Fork 14k
[FLINK-37925][table] Reject out-of-range VARIANT casts and make VARIANT-to-string a value cast #28758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
[FLINK-37925][table] Reject out-of-range VARIANT casts and make VARIANT-to-string a value cast #28758
Changes from all commits
8db28aa
2f826e6
c3dada9
fd2676f
db85cdf
23e41c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -1086,7 +1086,7 @@ The type can be declared using the above combinations where `p1` is the number o | |||||||
| and `9` (both inclusive). If no `p1` is specified, it is equal to `2` by default. If no `p2` is | ||||||||
| specified, it is equal to `6` by default. | ||||||||
|
|
||||||||
| ### Constructured Data Types | ||||||||
| ### Constructed Data Types | ||||||||
|
|
||||||||
| #### `ARRAY` | ||||||||
|
|
||||||||
|
|
@@ -1507,19 +1507,64 @@ close to the semantics of JSON. Compared to `ROW` and `STRUCTURED` type, `VARIAN | |||||||
| flexibility to support highly nested and evolving schema. | ||||||||
|
|
||||||||
| `VARIANT` allows for deeply nested data structures, such as arrays within arrays, maps within maps, | ||||||||
| or combinations of both.This capability makes `VARIANT` ideal for scenarios where data complexity | ||||||||
| or combinations of both. This capability makes `VARIANT` ideal for scenarios where data complexity | ||||||||
| and nesting are significant. | ||||||||
|
|
||||||||
| `VARIANT` allows schema evolution, enabling the storage of data with changing or unknown schemas | ||||||||
| without requiring upfront schema definition. For example, if a new field is added to the data, it | ||||||||
| can be directly incorporated into the `VARIANT` data without modifying the table schema. This is | ||||||||
| particularly useful in dynamic environments where schemas may evolve over time. | ||||||||
|
|
||||||||
| A `VARIANT` stores a single value of one of the following kinds: `NULL`, `BOOLEAN`, `TINYINT`, | ||||||||
| `SMALLINT`, `INT`, `BIGINT`, `FLOAT`, `DOUBLE`, `DECIMAL` (up to precision 38), `STRING`, `DATE`, | ||||||||
| `TIMESTAMP`, `TIMESTAMP_LTZ`, `BYTES`, or a nested array or object. `TIMESTAMP` and `TIMESTAMP_LTZ` | ||||||||
| are stored with microsecond precision and `DATE` as a day count. There is no `TIME` kind. | ||||||||
|
|
||||||||
| The `PARSE_JSON` function produces only the kinds that JSON syntax can express: | ||||||||
|
|
||||||||
| | JSON input | Stored `VARIANT` kind | | ||||||||
| |-----------------------------------------------------|-------------------------------------------------| | ||||||||
| | `null` | `NULL` | | ||||||||
| | `true` / `false` | `BOOLEAN` | | ||||||||
| | Integer within the 64-bit signed range | smallest of `TINYINT`/`SMALLINT`/`INT`/`BIGINT` | | ||||||||
| | Integer beyond 64-bit, up to 38 significant digits | `DECIMAL` | | ||||||||
| | Decimal in plain notation, up to precision/scale 38 | `DECIMAL` | | ||||||||
| | Number in scientific notation, e.g. `1.5e3` | `DOUBLE` | | ||||||||
| | Number exceeding 38 digits of precision or scale | `DOUBLE` | | ||||||||
| | String | `STRING` | | ||||||||
| | Array | array (elements encoded by the same rules) | | ||||||||
| | Object | object (values encoded by the same rules) | | ||||||||
|
|
||||||||
| Because JSON has no literal for float, date, timestamp, or binary values, `PARSE_JSON` never | ||||||||
| produces `FLOAT`, `DATE`, `TIMESTAMP`, `TIMESTAMP_LTZ`, or `BYTES`. A `VARIANT` can still hold | ||||||||
| those kinds when built by other producers, such as the programmatic `VariantBuilder` API or format | ||||||||
| conversions like Avro. | ||||||||
|
|
||||||||
| The JSON specification has no `NaN` or infinity literals, so `PARSE_JSON('NaN')`, | ||||||||
| `PARSE_JSON('Infinity')`, and `PARSE_JSON('-Infinity')` fail, and `TRY_PARSE_JSON` returns `NULL`. | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also include the |
||||||||
| A `VARIANT` has no dedicated kind for these values. To keep one, store it as a JSON string and cast | ||||||||
| it back out, for example `CAST(CAST(PARSE_JSON('"Infinity"') AS STRING) AS FLOAT)`. | ||||||||
|
|
||||||||
| A primitive-valued `VARIANT` can be converted to a scalar type with `CAST` or `TRY_CAST`. Numeric | ||||||||
| targets are lenient: a variant holding any numeric value casts to any numeric type, so a JSON integer | ||||||||
| such as `PARSE_JSON('42')` casts to `INT` or `BIGINT`. Other targets require the stored value to be of | ||||||||
| the matching kind. When the value cannot be converted, `CAST` fails the job and `TRY_CAST` returns | ||||||||
| `NULL`. Use the `JSON_STRING` function to obtain the JSON string representation of a `VARIANT`. | ||||||||
| targets accept any numeric value, so `PARSE_JSON('42')` casts to `INT`, `BIGINT`, or `DOUBLE`. A | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| value outside an integer or `DECIMAL` target's range fails `CAST` and returns `NULL` for | ||||||||
| `TRY_CAST`. Other targets require the stored value to be of the matching kind, and a mismatch is | ||||||||
| handled the same way. Casting a `VARIANT` to `CHAR`/`VARCHAR` extracts the scalar value, so a | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not go into the details of CHAR/VARCHAR at this line
Suggested change
|
||||||||
| stored string is returned unquoted (for example `foo`). A variant that holds an object, array, or | ||||||||
| binary value is not castable to a string; use `JSON_STRING` for its JSON representation instead | ||||||||
| (there a string stays quoted, for example `"foo"`). A bounded `CHAR(n)`/`VARCHAR(n)` target is length-checked strictly, with no | ||||||||
| padding or truncation: `VARCHAR(n)` accepts up to `n` characters and `CHAR(n)` requires exactly `n`, | ||||||||
| otherwise `CAST` fails and `TRY_CAST` returns `NULL`. | ||||||||
|
Comment on lines
+1556
to
+1557
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. people should know how they work already
Suggested change
|
||||||||
|
|
||||||||
| {{< hint info >}} | ||||||||
| Unlike a regular numeric cast, casting a numeric `VARIANT` never overflows. To narrow a | ||||||||
| `VARIANT`, first cast it to a type that fits the stored value, then apply a | ||||||||
| regular narrowing cast: | ||||||||
|
|
||||||||
| ```sql | ||||||||
| CAST(CAST(PARSE_JSON('1000') AS INT) AS TINYINT) -- returns -24 (after overflow) | ||||||||
| ``` | ||||||||
| {{< /hint >}} | ||||||||
|
Comment on lines
+1564
to
+1567
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is difference here from non VARIANT case?
Comment on lines
+1559
to
+1567
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would vote for dropping this paragraph entirely. Or make it less prominent. A |
||||||||
|
|
||||||||
| **Declaration** | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1094,7 +1094,7 @@ The type can be declared using the above combinations where `p1` is the number o | |
| and `9` (both inclusive). If no `p1` is specified, it is equal to `2` by default. If no `p2` is | ||
| specified, it is equal to `6` by default. | ||
|
|
||
| ### Constructured Data Types | ||
| ### Constructed Data Types | ||
|
|
||
| #### `ARRAY` | ||
|
|
||
|
|
@@ -1515,19 +1515,64 @@ close to the semantics of JSON. Compared to `ROW` and `STRUCTURED` type, `VARIAN | |
| flexibility to support highly nested and evolving schema. | ||
|
|
||
| `VARIANT` allows for deeply nested data structures, such as arrays within arrays, maps within maps, | ||
| or combinations of both.This capability makes `VARIANT` ideal for scenarios where data complexity | ||
| or combinations of both. This capability makes `VARIANT` ideal for scenarios where data complexity | ||
| and nesting are significant. | ||
|
|
||
| `VARIANT` allows schema evolution, enabling the storage of data with changing or unknown schemas | ||
| without requiring upfront schema definition. For example, if a new field is added to the data, it | ||
| can be directly incorporated into the `VARIANT` data without modifying the table schema. This is | ||
| particularly useful in dynamic environments where schemas may evolve over time. | ||
|
|
||
| A primitive-valued `VARIANT` can be converted to a scalar type with `CAST` or `TRY_CAST`. Numeric | ||
| targets are lenient: a variant holding any numeric value casts to any numeric type, so a JSON integer | ||
| such as `PARSE_JSON('42')` casts to `INT` or `BIGINT`. Other targets require the stored value to be of | ||
| the matching kind. When the value cannot be converted, `CAST` fails the job and `TRY_CAST` returns | ||
| `NULL`. Use the `JSON_STRING` function to obtain the JSON string representation of a `VARIANT`. | ||
| A `VARIANT` stores a single value of one of the following kinds: `NULL`, `BOOLEAN`, `TINYINT`, | ||
| `SMALLINT`, `INT`, `BIGINT`, `FLOAT`, `DOUBLE`, `DECIMAL` (up to precision 38), `STRING`, `DATE`, | ||
| `TIMESTAMP`, `TIMESTAMP_LTZ`, `BYTES`, or a nested array or object. `TIMESTAMP` and `TIMESTAMP_LTZ` | ||
| are stored with microsecond precision and `DATE` as a day count. There is no `TIME` kind. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why microsecond not nanoseconds?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @davidradl This is how the implementation of the @Override
public Instant getInstant() throws VariantTypeException {
checkType(Type.TIMESTAMP_LTZ, getType());
return microsToInstant(BinaryVariantUtil.getLong(value, pos));
}The implementation is done based on the Encoding types defined in Parquet. Flink implements the types from |
||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am curious why Date does not go to Flink date which would seem more natural
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @davidradl I think the implementation of
@Override
public LocalDate getDate() throws VariantTypeException {
checkType(Type.DATE, getType());
return LocalDate.ofEpochDay(BinaryVariantUtil.getLong(value, pos));
} |
||
| The `PARSE_JSON` function produces only the kinds that JSON syntax can express: | ||
|
|
||
| | JSON input | Stored `VARIANT` kind | | ||
| |-----------------------------------------------------|-------------------------------------------------| | ||
| | `null` | `NULL` | | ||
| | `true` / `false` | `BOOLEAN` | | ||
| | Integer within the 64-bit signed range | smallest of `TINYINT`/`SMALLINT`/`INT`/`BIGINT` | | ||
| | Integer beyond 64-bit, up to 38 significant digits | `DECIMAL` | | ||
| | Decimal in plain notation, up to precision/scale 38 | `DECIMAL` | | ||
| | Number in scientific notation, e.g. `1.5e3` | `DOUBLE` | | ||
| | Number exceeding 38 digits of precision or scale | `DOUBLE` | | ||
| | String | `STRING` | | ||
| | Array | array (elements encoded by the same rules) | | ||
| | Object | object (values encoded by the same rules) | | ||
|
|
||
| Because JSON has no literal for float, date, timestamp, or binary values, `PARSE_JSON` never | ||
| produces `FLOAT`, `DATE`, `TIMESTAMP`, `TIMESTAMP_LTZ`, or `BYTES`. A `VARIANT` can still hold | ||
| those kinds when built by other producers, such as the programmatic `VariantBuilder` API or format | ||
| conversions like Avro. | ||
|
|
||
| The JSON specification has no `NaN` or infinity literals, so `PARSE_JSON('NaN')`, | ||
| `PARSE_JSON('Infinity')`, and `PARSE_JSON('-Infinity')` fail, and `TRY_PARSE_JSON` returns `NULL`. | ||
| A `VARIANT` has no dedicated kind for these values. To keep one, store it as a JSON string and cast | ||
| it back out, for example `CAST(CAST(PARSE_JSON('"Infinity"') AS STRING) AS FLOAT)`. | ||
|
|
||
| A primitive-valued `VARIANT` can be converted to a scalar type with `CAST` or `TRY_CAST`. Numeric | ||
| targets accept any numeric value, so `PARSE_JSON('42')` casts to `INT`, `BIGINT`, or `DOUBLE`. A | ||
| value outside an integer or `DECIMAL` target's range fails `CAST` and returns `NULL` for | ||
| `TRY_CAST`. Other targets require the stored value to be of the matching kind, and a mismatch is | ||
| handled the same way. Casting a `VARIANT` to `CHAR`/`VARCHAR` extracts the scalar value, so a | ||
| stored string is returned unquoted (for example `foo`). A variant that holds an object, array, or | ||
| binary value is not castable to a string; use `JSON_STRING` for its JSON representation instead | ||
| (there a string stays quoted, for example `"foo"`). A bounded `CHAR(n)`/`VARCHAR(n)` target is length-checked strictly, with no | ||
| padding or truncation: `VARCHAR(n)` accepts up to `n` characters and `CHAR(n)` requires exactly `n`, | ||
| otherwise `CAST` fails and `TRY_CAST` returns `NULL`. | ||
|
|
||
| {{< hint info >}} | ||
| Unlike a regular numeric cast, casting a numeric `VARIANT` never overflows. To narrow a | ||
| `VARIANT`, first cast it to a type that fits the stored value, then apply a | ||
| regular narrowing cast: | ||
|
|
||
| ```sql | ||
| CAST(CAST(PARSE_JSON('1000') AS INT) AS TINYINT) -- returns -24 (after overflow) | ||
| ``` | ||
| {{< /hint >}} | ||
|
|
||
| **Declaration** | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,15 +34,7 @@ public static void main(String[] args) throws Exception { | |
| // execute a Flink SQL job and print the result locally | ||
| tableEnv.executeSql( | ||
| // define the aggregation | ||
| "SELECT word, SUM(frequency) AS `count`\n" | ||
| // read from an artificial fixed-size table with rows and columns | ||
| + "FROM (\n" | ||
| + " VALUES ('Hello', 1), ('Ciao', 1), ('Hello', 2)\n" | ||
| + ")\n" | ||
| // name the table and its columns | ||
| + "AS WordTable(word, frequency)\n" | ||
| // group for aggregation | ||
| + "GROUP BY word") | ||
| "SELECT PARSE_JSON('Infinity')") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undo |
||
| .print(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.