This is an automated email from the ASF dual-hosted git repository. raminqaf pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/flink.git
commit c59810c04f830f8d6d2d7a2fc77dcbd03b29e714 Author: Ramin Gharib <[email protected]> AuthorDate: Tue Nov 3 16:42:39 2026 +0100 [FLINK-41323][table-runtime] Add a binary encoding for a VARIANT column in the raw format The raw format reads and writes a VARIANT column as a JSON document. JSON has no type for dates, times, timestamps, bytes or UUIDs, so a round trip turns those values into strings. Every message is also parsed on read and rendered on write. The new option 'raw.variant-encoding' takes 'json', the default, or 'binary'. With 'binary', a message holds the Variant binary encoding, the metadata followed by the value. The metadata states its own length, so the reader splits the message without a separator and wraps the two parts without parsing. Every Variant type survives the round trip. The bytes are only checked as far as needed to split them, so a malformed value fails when it is read. 'binary' ignores 'raw.charset'. The docs call the JSON round trip lossy, and recommend 'binary' for correctness and performance and 'json' for convenience. Generated-by: Claude Code 2.1.316 (Claude Opus 5.5) --- .../docs/connectors/table/formats/raw.md | 31 +++++++- docs/content/docs/connectors/table/formats/raw.md | 31 +++++++- .../flink/types/variant/BinaryVariantUtil.java | 27 +++++++ .../flink/types/variant/BinaryVariantTest.java | 26 +++++++ .../raw/RawFormatDeserializationSchema.java | 66 ++++++++++++---- .../apache/flink/formats/raw/RawFormatFactory.java | 38 ++++++++-- .../apache/flink/formats/raw/RawFormatOptions.java | 41 ++++++++++ .../formats/raw/RawFormatSerializationSchema.java | 46 ++++++++--- .../table/formats/raw/RawFormatFactoryTest.java | 73 ++++++++++++++++-- .../formats/raw/RawFormatLineDelimiterTest.java | 65 ++++++++++++---- .../formats/raw/RawFormatSerDeSchemaTest.java | 88 +++++++++++++++++++++- 11 files changed, 465 insertions(+), 67 deletions(-) diff --git a/docs/content.zh/docs/connectors/table/formats/raw.md b/docs/content.zh/docs/connectors/table/formats/raw.md index 1d728c3fadb..bcfccf3956d 100644 --- a/docs/content.zh/docs/connectors/table/formats/raw.md +++ b/docs/content.zh/docs/connectors/table/formats/raw.md @@ -119,6 +119,20 @@ Format 参数 <br><strong>注意:</strong>当序列化与反序列化使用相同的分隔符配置时,两者具有 round-trip 兼容性:序列化器追加的末尾分隔符会在反序列化时自动被去除。</td> </tr> + <tr> + <td><h5>raw.variant-encoding</h5></td> + <td>可选</td> + <td style="word-wrap: break-word;">json</td> + <td>Enum</td> + <td>Specify how a <code>VARIANT</code> column is encoded. Valid values are 'json' and 'binary'. + 'json' reads and writes a UTF-8 JSON document. For convenience, and also because JSON is a very + popular and human readable format, the stored VARIANT value is assumed to be JSON. However, JSON + values are lossy: JSON has no type for dates, times, timestamps, bytes or + UUIDs, and it does not keep the width of a number. 'binary' reads and writes the Variant binary + encoding, the metadata followed by the value. It keeps every Variant type and skips JSON parsing + and rendering, so use it for correctness and performance. 'raw.charset' does not apply to + 'binary'.</td> + </tr> </tbody> </table> @@ -183,13 +197,22 @@ Format 参数 </tr> <tr> <td><code>VARIANT</code></td> - <td>A UTF-8 encoded JSON document.<br> + <td>Depends on 'raw.variant-encoding'.<br> + <strong>json</strong> (default): a UTF-8 encoded JSON document. 'raw.charset' must be UTF-8. To read or write JSON in another charset, declare the column as <code>STRING</code> and convert it with <code>PARSE_JSON</code> when reading and <code>JSON_STRING</code> - when writing.<br> + when writing. On read, the bytes are parsed like <code>PARSE_JSON</code> with the same checks, so an invalid UTF-8 byte such as - <code>0xFF</code>, a duplicate object key or malformed JSON fails the job. On write, the value is rendered by <code>Variant#toJson</code>. The round trip is - value-lossless but not byte-lossless: insignificant whitespace is dropped and object keys are ordered.</td> + <code>0xFF</code>, a duplicate object key or malformed JSON fails the job. On write, the value is rendered by + <code>Variant#toJson</code>. + The round trip is lossy, because JSON has fewer types than VARIANT: a <code>DATE</code>, <code>TIME</code>, + <code>TIMESTAMP</code>, <code>BYTES</code> or <code>UUID</code> value comes back as a string, a <code>FLOAT</code> + or <code>DOUBLE</code> can come back as a <code>DECIMAL</code>, and an integer comes back as the smallest integer + type that holds it. + Insignificant whitespace is dropped and object keys are ordered.<br> + <strong>binary</strong>: the Variant binary encoding, the metadata followed by the value. The round trip keeps + every Variant type. The bytes are only checked as far as needed to split them, so a malformed value fails when + it is read.</td> </tr> </tbody> </table> diff --git a/docs/content/docs/connectors/table/formats/raw.md b/docs/content/docs/connectors/table/formats/raw.md index 2f183e53f44..42ef9b54c77 100644 --- a/docs/content/docs/connectors/table/formats/raw.md +++ b/docs/content/docs/connectors/table/formats/raw.md @@ -122,6 +122,20 @@ Format Options deserialization, the two are round-trip compatible: a trailing delimiter appended by the serializer is automatically stripped during deserialization.</td> </tr> + <tr> + <td><h5>raw.variant-encoding</h5></td> + <td>optional</td> + <td style="word-wrap: break-word;">json</td> + <td>Enum</td> + <td>Specify how a <code>VARIANT</code> column is encoded. Valid values are 'json' and 'binary'. + 'json' reads and writes a UTF-8 JSON document. For convenience, and also because JSON is a very + popular and human readable format, the stored VARIANT value is assumed to be JSON. However, JSON + values are lossy: JSON has no type for dates, times, timestamps, bytes or + UUIDs, and it does not keep the width of a number. 'binary' reads and writes the Variant binary + encoding, the metadata followed by the value. It keeps every Variant type and skips JSON parsing + and rendering, so use it for correctness and performance. 'raw.charset' does not apply to + 'binary'.</td> + </tr> </tbody> </table> @@ -186,13 +200,22 @@ The table below details the SQL types the format supports, including details of </tr> <tr> <td><code>VARIANT</code></td> - <td>A UTF-8 encoded JSON document.<br> + <td>Depends on 'raw.variant-encoding'.<br> + <strong>json</strong> (default): a UTF-8 encoded JSON document. 'raw.charset' must be UTF-8. To read or write JSON in another charset, declare the column as <code>STRING</code> and convert it with <code>PARSE_JSON</code> when reading and <code>JSON_STRING</code> - when writing.<br> + when writing. On read, the bytes are parsed like <code>PARSE_JSON</code> with the same checks, so an invalid UTF-8 byte such as - <code>0xFF</code>, a duplicate object key or malformed JSON fails the job. On write, the value is rendered by <code>Variant#toJson</code>. The round trip is - value-lossless but not byte-lossless: insignificant whitespace is dropped and object keys are ordered.</td> + <code>0xFF</code>, a duplicate object key or malformed JSON fails the job. On write, the value is rendered by + <code>Variant#toJson</code>. + The round trip is lossy, because JSON has fewer types than VARIANT: a <code>DATE</code>, <code>TIME</code>, + <code>TIMESTAMP</code>, <code>BYTES</code> or <code>UUID</code> value comes back as a string, a <code>FLOAT</code> + or <code>DOUBLE</code> can come back as a <code>DECIMAL</code>, and an integer comes back as the smallest integer + type that holds it. + Insignificant whitespace is dropped and object keys are ordered.<br> + <strong>binary</strong>: the Variant binary encoding, the metadata followed by the value. The round trip keeps + every Variant type. The bytes are only checked as far as needed to split them, so a malformed value fails when + it is read.</td> </tr> </tbody> </table> diff --git a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java index 85d13a9f501..38f7eaebaa5 100644 --- a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java +++ b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java @@ -913,6 +913,33 @@ public class BinaryVariantUtil { return false; } + /** + * Returns the length of the metadata at the start of {@code bytes}. The header, dictionary size + * and last offset give the length, so metadata followed by a value can be split without a + * separator. + * + * @throws VariantTypeException if {@code bytes} does not start with complete metadata + */ + public static int metadataSize(byte[] bytes) { + checkIndex(0, bytes.length); + if ((bytes[0] & VERSION_MASK) != VERSION) { + throw malformedVariant(); + } + final int offsetSize = ((bytes[0] >> 6) & 0x3) + 1; + final int dictSize = readUnsigned(bytes, 1, offsetSize); + // The last of the `dictSize + 1` offsets is the length of the string data. + final long lastOffsetPos = 1 + (dictSize + 1L) * offsetSize; + if (lastOffsetPos + offsetSize > bytes.length) { + throw malformedVariant(); + } + final long size = + lastOffsetPos + offsetSize + readUnsigned(bytes, (int) lastOffsetPos, offsetSize); + if (size > bytes.length) { + throw malformedVariant(); + } + return (int) size; + } + /** * Locates the key at {@code id} in the variant metadata. The range is packed into a long so * that a lookup allocates nothing. diff --git a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java index 961c2ff4a4d..c9976e067c2 100644 --- a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java +++ b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java @@ -19,6 +19,7 @@ package org.apache.flink.types.variant; import org.apache.flink.core.testutils.CommonTestUtils; +import org.apache.flink.util.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,6 +38,7 @@ import java.time.LocalTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -635,6 +637,30 @@ class BinaryVariantTest { .hasMessage("VARIANT_CONSTRUCTOR_SIZE_LIMIT"); } + @ParameterizedTest + @CsvSource({"1, 1", "300, 2", "70000, 3"}) + void testMetadataSize(final int keyLength, final int offsetSize) { + final BinaryVariant variant = + (BinaryVariant) + builder.object().add("k".repeat(keyLength), builder.of((byte) 1)).build(); + final byte[] metadata = variant.getMetadata(); + final byte[] value = variant.getValue(); + final byte[] bytes = Arrays.copyOf(metadata, metadata.length + value.length); + System.arraycopy(value, 0, bytes, metadata.length, value.length); + + assertThat(((metadata[0] >> 6) & 0x3) + 1).isEqualTo(offsetSize); + assertThat(BinaryVariantUtil.metadataSize(bytes)).isEqualTo(metadata.length); + } + + // Empty, version 2, a dictionary of one key without offsets, and a last offset past the end. + @ParameterizedTest + @ValueSource(strings = {"", "020000", "0101", "010005"}) + void testMetadataSizeRejectsIncompleteMetadata(final String hex) { + assertThatThrownBy(() -> BinaryVariantUtil.metadataSize(StringUtils.hexStringToByte(hex))) + .isInstanceOf(VariantTypeException.class) + .hasMessage("MALFORMED_VARIANT"); + } + @Test void testGetThrowException() { Variant variant = builder.of(10f); diff --git a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatDeserializationSchema.java b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatDeserializationSchema.java index b89881eee49..c8945d27d82 100644 --- a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatDeserializationSchema.java +++ b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatDeserializationSchema.java @@ -23,13 +23,16 @@ import org.apache.flink.api.common.serialization.DeserializationSchema; import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.core.memory.MemorySegment; import org.apache.flink.core.memory.MemorySegmentFactory; +import org.apache.flink.formats.raw.RawFormatOptions.VariantEncoding; import org.apache.flink.table.data.GenericRowData; import org.apache.flink.table.data.RawValueData; import org.apache.flink.table.data.RowData; import org.apache.flink.table.data.StringData; import org.apache.flink.table.types.logical.LogicalType; import org.apache.flink.types.DeserializationException; +import org.apache.flink.types.variant.BinaryVariant; import org.apache.flink.types.variant.BinaryVariantInternalBuilder; +import org.apache.flink.types.variant.BinaryVariantUtil; import org.apache.flink.types.variant.Variant; import org.apache.flink.util.Collector; @@ -65,27 +68,25 @@ public class RawFormatDeserializationSchema implements DeserializationSchema<Row @Nullable private final String lineDelimiter; + private final VariantEncoding variantEncoding; + private final DeserializationRuntimeConverter converter; private final DataLengthValidator validator; - public RawFormatDeserializationSchema( - LogicalType deserializedType, - TypeInformation<RowData> producedTypeInfo, - String charsetName, - boolean isBigEndian) { - this(deserializedType, producedTypeInfo, charsetName, isBigEndian, null); - } - public RawFormatDeserializationSchema( LogicalType deserializedType, TypeInformation<RowData> producedTypeInfo, String charsetName, boolean isBigEndian, - @Nullable String lineDelimiter) { + @Nullable String lineDelimiter, + VariantEncoding variantEncoding) { this.deserializedType = checkNotNull(deserializedType); this.producedTypeInfo = checkNotNull(producedTypeInfo); - this.converter = createConverter(deserializedType, charsetName, isBigEndian, lineDelimiter); + this.variantEncoding = checkNotNull(variantEncoding); + this.converter = + createConverter( + deserializedType, charsetName, isBigEndian, lineDelimiter, variantEncoding); this.validator = createDataLengthValidator(deserializedType); this.charsetName = charsetName; this.isBigEndian = isBigEndian; @@ -159,13 +160,19 @@ public class RawFormatDeserializationSchema implements DeserializationSchema<Row && deserializedType.equals(that.deserializedType) && charsetName.equals(that.charsetName) && isBigEndian == that.isBigEndian - && Objects.equals(lineDelimiter, that.lineDelimiter); + && Objects.equals(lineDelimiter, that.lineDelimiter) + && variantEncoding == that.variantEncoding; } @Override public int hashCode() { return Objects.hash( - producedTypeInfo, deserializedType, charsetName, isBigEndian, lineDelimiter); + producedTypeInfo, + deserializedType, + charsetName, + isBigEndian, + lineDelimiter, + variantEncoding); } // ------------------------------------------------------------------------ @@ -192,7 +199,8 @@ public class RawFormatDeserializationSchema implements DeserializationSchema<Row LogicalType type, String charsetName, boolean isBigEndian, - @Nullable String lineDelimiter) { + @Nullable String lineDelimiter, + VariantEncoding variantEncoding) { switch (type.getTypeRoot()) { case CHAR: @@ -209,7 +217,9 @@ public class RawFormatDeserializationSchema implements DeserializationSchema<Row return RawValueData::fromBytes; case VARIANT: - return createVariantConverter(); + return variantEncoding == VariantEncoding.BINARY + ? createBinaryVariantConverter() + : createJsonVariantConverter(); case BOOLEAN: return data -> data[0] != 0; @@ -357,7 +367,7 @@ public class RawFormatDeserializationSchema implements DeserializationSchema<Row * Creates a converter that parses UTF-8 JSON bytes into a {@link Variant} like {@code * PARSE_JSON}, so invalid UTF-8 and duplicate object keys are rejected. */ - private static DeserializationRuntimeConverter createVariantConverter() { + private static DeserializationRuntimeConverter createJsonVariantConverter() { return new DeserializationRuntimeConverter() { private static final long serialVersionUID = 1L; @@ -375,6 +385,32 @@ public class RawFormatDeserializationSchema implements DeserializationSchema<Row }; } + /** + * Creates a converter that reads the Variant binary encoding, the metadata followed by the + * value. The bytes are only checked as far as needed to split them. A malformed value fails + * when it is read. + */ + private static DeserializationRuntimeConverter createBinaryVariantConverter() { + return new DeserializationRuntimeConverter() { + private static final long serialVersionUID = 1L; + + @Override + public Object convert(byte[] data) { + try { + final int metadataSize = BinaryVariantUtil.metadataSize(data); + return new BinaryVariant( + Arrays.copyOfRange(data, metadataSize, data.length), + Arrays.copyOf(data, metadataSize)); + } catch (Exception e) { + throw new DeserializationException( + "Failed to deserialize VARIANT type. " + + "The received data is not a valid VARIANT binary encoding.", + e); + } + } + }; + } + private static DeserializationRuntimeConverter createEndiannessAwareConverter( final boolean isBigEndian, final MemorySegmentConverter bigEndianConverter, diff --git a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatFactory.java b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatFactory.java index bf2899d64ec..48571c1de82 100644 --- a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatFactory.java +++ b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatFactory.java @@ -24,6 +24,7 @@ import org.apache.flink.api.common.serialization.SerializationSchema; import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.configuration.ConfigOption; import org.apache.flink.configuration.ReadableConfig; +import org.apache.flink.formats.raw.RawFormatOptions.VariantEncoding; import org.apache.flink.table.api.ValidationException; import org.apache.flink.table.connector.ChangelogMode; import org.apache.flink.table.connector.format.DecodingFormat; @@ -76,6 +77,7 @@ public class RawFormatFactory implements DeserializationFormatFactory, Serializa options.add(RawFormatOptions.ENDIANNESS); options.add(RawFormatOptions.CHARSET); options.add(RawFormatOptions.LINE_DELIMITER); + options.add(RawFormatOptions.VARIANT_ENCODING); return options; } @@ -87,6 +89,8 @@ public class RawFormatFactory implements DeserializationFormatFactory, Serializa final boolean isBigEndian = isBigEndian(formatOptions); final Optional<String> lineDelimiter = formatOptions.getOptional(RawFormatOptions.LINE_DELIMITER); + final VariantEncoding variantEncoding = + formatOptions.get(RawFormatOptions.VARIANT_ENCODING); return new DecodingFormat<DeserializationSchema<RowData>>() { @Override @@ -95,7 +99,7 @@ public class RawFormatFactory implements DeserializationFormatFactory, Serializa final RowType physicalRowType = (RowType) producedDataType.getLogicalType(); final LogicalType fieldType = validateAndExtractSingleField(physicalRowType); checkLineDelimiter(fieldType, lineDelimiter); - checkVariantCharset(fieldType, charsetName); + checkVariantOptions(fieldType, charsetName, variantEncoding); final TypeInformation<RowData> producedTypeInfo = context.createTypeInformation(producedDataType); return new RawFormatDeserializationSchema( @@ -103,7 +107,8 @@ public class RawFormatFactory implements DeserializationFormatFactory, Serializa producedTypeInfo, charsetName, isBigEndian, - lineDelimiter.orElse(null)); + lineDelimiter.orElse(null), + variantEncoding); } @Override @@ -121,6 +126,8 @@ public class RawFormatFactory implements DeserializationFormatFactory, Serializa final boolean isBigEndian = isBigEndian(formatOptions); final Optional<String> lineDelimiter = formatOptions.getOptional(RawFormatOptions.LINE_DELIMITER); + final VariantEncoding variantEncoding = + formatOptions.get(RawFormatOptions.VARIANT_ENCODING); return new EncodingFormat<SerializationSchema<RowData>>() { @Override @@ -129,9 +136,13 @@ public class RawFormatFactory implements DeserializationFormatFactory, Serializa final RowType physicalRowType = (RowType) consumedDataType.getLogicalType(); final LogicalType fieldType = validateAndExtractSingleField(physicalRowType); checkLineDelimiter(fieldType, lineDelimiter); - checkVariantCharset(fieldType, charsetName); + checkVariantOptions(fieldType, charsetName, variantEncoding); return new RawFormatSerializationSchema( - fieldType, charsetName, isBigEndian, lineDelimiter.orElse(null)); + fieldType, + charsetName, + isBigEndian, + lineDelimiter.orElse(null), + variantEncoding); } @Override @@ -185,13 +196,26 @@ public class RawFormatFactory implements DeserializationFormatFactory, Serializa } } + /** + * Checks the options of a VARIANT column. The binary encoding is not text, so the charset does + * not apply to it. + */ + private static void checkVariantOptions( + LogicalType fieldType, String charsetName, VariantEncoding variantEncoding) { + if (!fieldType.is(LogicalTypeRoot.VARIANT)) { + return; + } + if (variantEncoding == VariantEncoding.JSON) { + checkVariantCharset(charsetName); + } + } + /** * Checks that a VARIANT column uses UTF-8, which RFC 8259 requires for JSON exchanged between * systems and which {@code PARSE_JSON} reads. */ - private static void checkVariantCharset(LogicalType fieldType, String charsetName) { - if (fieldType.is(LogicalTypeRoot.VARIANT) - && !StandardCharsets.UTF_8.equals(Charset.forName(charsetName))) { + private static void checkVariantCharset(String charsetName) { + if (!StandardCharsets.UTF_8.equals(Charset.forName(charsetName))) { throw new ValidationException( String.format( "The 'raw' format requires '%s.%s' to be UTF-8 for a VARIANT column, " diff --git a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatOptions.java b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatOptions.java index 7fa674b7f98..7ca5e46a9e1 100644 --- a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatOptions.java +++ b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatOptions.java @@ -21,9 +21,13 @@ package org.apache.flink.formats.raw; import org.apache.flink.annotation.PublicEvolving; import org.apache.flink.configuration.ConfigOption; import org.apache.flink.configuration.ConfigOptions; +import org.apache.flink.configuration.DescribedEnum; +import org.apache.flink.configuration.description.InlineElement; import java.nio.charset.StandardCharsets; +import static org.apache.flink.configuration.description.TextElement.text; + /** Options for the "raw" format. */ @PublicEvolving public class RawFormatOptions { @@ -52,5 +56,42 @@ public class RawFormatOptions { + "When set, deserialization splits each message by this delimiter and emits " + "one RowData per part. Serialization appends the delimiter after each row's value."); + public static final ConfigOption<VariantEncoding> VARIANT_ENCODING = + ConfigOptions.key("variant-encoding") + .enumType(VariantEncoding.class) + .defaultValue(VariantEncoding.JSON) + .withDescription( + "Defines how a VARIANT column is encoded. 'json' reads and writes a UTF-8 " + + "JSON document, which is convenient but lossy: JSON has no type " + + "for dates, times, timestamps, bytes or UUIDs. 'binary' reads and " + + "writes the Variant binary encoding, the metadata followed by the " + + "value, which keeps every Variant type and skips JSON parsing. " + + "'charset' does not apply to 'binary'."); + + /** Encoding of a VARIANT column, see {@link #VARIANT_ENCODING}. */ + @PublicEvolving + public enum VariantEncoding implements DescribedEnum { + JSON("json", text("A UTF-8 JSON document. Types JSON cannot represent are lost.")), + BINARY("binary", text("The Variant binary encoding, the metadata followed by the value.")); + + private final String value; + private final InlineElement description; + + VariantEncoding(String value, InlineElement description) { + this.value = value; + this.description = description; + } + + @Override + public String toString() { + return value; + } + + @Override + public InlineElement getDescription() { + return description; + } + } + private RawFormatOptions() {} } diff --git a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatSerializationSchema.java b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatSerializationSchema.java index 1661b154bbd..02d2f4bee79 100644 --- a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatSerializationSchema.java +++ b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/formats/raw/RawFormatSerializationSchema.java @@ -23,9 +23,11 @@ import org.apache.flink.api.common.serialization.SerializationSchema; import org.apache.flink.api.common.typeutils.TypeSerializer; import org.apache.flink.core.memory.MemorySegment; import org.apache.flink.core.memory.MemorySegmentFactory; +import org.apache.flink.formats.raw.RawFormatOptions.VariantEncoding; import org.apache.flink.table.data.RowData; import org.apache.flink.table.types.logical.LogicalType; import org.apache.flink.table.types.logical.RawType; +import org.apache.flink.types.variant.BinaryVariant; import org.apache.flink.types.variant.Variant; import org.apache.flink.types.variant.VariantTypeException; @@ -38,6 +40,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Objects; +import static org.apache.flink.util.Preconditions.checkNotNull; + /** Serialization schema that serializes an {@link RowData} object into raw (byte based) value. */ @Internal public class RawFormatSerializationSchema implements SerializationSchema<RowData> { @@ -57,17 +61,16 @@ public class RawFormatSerializationSchema implements SerializationSchema<RowData /** Pre-computed delimiter bytes, or {@code null} if no delimiter is set. */ @Nullable private final byte[] delimiterBytes; - public RawFormatSerializationSchema( - LogicalType serializedType, String charsetName, boolean isBigEndian) { - this(serializedType, charsetName, isBigEndian, null); - } + private final VariantEncoding variantEncoding; public RawFormatSerializationSchema( LogicalType serializedType, String charsetName, boolean isBigEndian, - @Nullable String lineDelimiter) { + @Nullable String lineDelimiter, + VariantEncoding variantEncoding) { this.serializedType = serializedType; + this.variantEncoding = checkNotNull(variantEncoding); this.converter = createConverter(serializedType, charsetName, isBigEndian); this.charsetName = charsetName; this.isBigEndian = isBigEndian; @@ -108,12 +111,14 @@ public class RawFormatSerializationSchema implements SerializationSchema<RowData return serializedType.equals(that.serializedType) && charsetName.equals(that.charsetName) && isBigEndian == that.isBigEndian - && Objects.equals(lineDelimiter, that.lineDelimiter); + && Objects.equals(lineDelimiter, that.lineDelimiter) + && variantEncoding == that.variantEncoding; } @Override public int hashCode() { - return Objects.hash(serializedType, charsetName, isBigEndian, lineDelimiter); + return Objects.hash( + serializedType, charsetName, isBigEndian, lineDelimiter, variantEncoding); } // ------------------------------------------------------------------------ @@ -167,7 +172,9 @@ public class RawFormatSerializationSchema implements SerializationSchema<RowData return createRawValueConverter((RawType<?>) type); case VARIANT: - return createVariantConverter(charsetName); + return variantEncoding == VariantEncoding.BINARY + ? createBinaryVariantConverter() + : createJsonVariantConverter(charsetName); case BOOLEAN: return row -> { @@ -226,10 +233,12 @@ public class RawFormatSerializationSchema implements SerializationSchema<RowData } /** - * Creates a converter that renders the {@link Variant} as a JSON document. The result is - * value-lossless but not byte-lossless: whitespace and object key order are normalized. + * Creates a converter that renders the {@link Variant} as a JSON document. JSON has fewer types + * than a Variant, so a date, timestamp, byte string or UUID is written as a string. Whitespace + * and object key order are normalized. */ - private static SerializationRuntimeConverter createVariantConverter(final String charsetName) { + private static SerializationRuntimeConverter createJsonVariantConverter( + final String charsetName) { return new SerializationRuntimeConverter() { private static final long serialVersionUID = 1L; private transient Charset charset; @@ -246,6 +255,21 @@ public class RawFormatSerializationSchema implements SerializationSchema<RowData }; } + /** + * Creates a converter that writes the Variant binary encoding, the metadata followed by the + * value. It keeps every Variant type, unlike the JSON document. + */ + private static SerializationRuntimeConverter createBinaryVariantConverter() { + return row -> { + final BinaryVariant variant = (BinaryVariant) row.getVariant(0); + final byte[] metadata = variant.getMetadata(); + final byte[] value = variant.getValue(); + final byte[] result = Arrays.copyOf(metadata, metadata.length + value.length); + System.arraycopy(value, 0, result, metadata.length, value.length); + return result; + }; + } + @SuppressWarnings("unchecked") private static SerializationRuntimeConverter createRawValueConverter(RawType<?> rawType) { final TypeSerializer<Object> serializer = diff --git a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatFactoryTest.java b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatFactoryTest.java index 92e95efa688..a174770e9df 100644 --- a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatFactoryTest.java +++ b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatFactoryTest.java @@ -22,6 +22,7 @@ import org.apache.flink.api.common.serialization.DeserializationSchema; import org.apache.flink.api.common.serialization.SerializationSchema; import org.apache.flink.formats.raw.RawFormatDeserializationSchema; import org.apache.flink.formats.raw.RawFormatFactory; +import org.apache.flink.formats.raw.RawFormatOptions.VariantEncoding; import org.apache.flink.formats.raw.RawFormatSerializationSchema; import org.apache.flink.table.api.DataTypes; import org.apache.flink.table.catalog.Column; @@ -69,14 +70,20 @@ class RawFormatFactoryTest { // test deserialization final RawFormatDeserializationSchema expectedDeser = new RawFormatDeserializationSchema( - ROW_TYPE.getTypeAt(0), InternalTypeInfo.of(ROW_TYPE), UTF_8.name(), true); + ROW_TYPE.getTypeAt(0), + InternalTypeInfo.of(ROW_TYPE), + UTF_8.name(), + true, + null, + VariantEncoding.JSON); DeserializationSchema<RowData> actualDeser = createDeserializationSchema(SCHEMA, tableOptions); assertThat(actualDeser).isEqualTo(expectedDeser); // test serialization final RawFormatSerializationSchema expectedSer = - new RawFormatSerializationSchema(ROW_TYPE.getTypeAt(0), UTF_8.name(), true); + new RawFormatSerializationSchema( + ROW_TYPE.getTypeAt(0), UTF_8.name(), true, null, VariantEncoding.JSON); SerializationSchema<RowData> actualSer = createSerializationSchema(SCHEMA, tableOptions); assertThat(actualSer).isEqualTo(expectedSer); } @@ -93,14 +100,20 @@ class RawFormatFactoryTest { // test deserialization final RawFormatDeserializationSchema expectedDeser = new RawFormatDeserializationSchema( - ROW_TYPE.getTypeAt(0), InternalTypeInfo.of(ROW_TYPE), UTF_16.name(), false); + ROW_TYPE.getTypeAt(0), + InternalTypeInfo.of(ROW_TYPE), + UTF_16.name(), + false, + null, + VariantEncoding.JSON); DeserializationSchema<RowData> actualDeser = createDeserializationSchema(SCHEMA, tableOptions); assertThat(actualDeser).isEqualTo(expectedDeser); // test serialization final RawFormatSerializationSchema expectedSer = - new RawFormatSerializationSchema(ROW_TYPE.getTypeAt(0), UTF_16.name(), false); + new RawFormatSerializationSchema( + ROW_TYPE.getTypeAt(0), UTF_16.name(), false, null, VariantEncoding.JSON); SerializationSchema<RowData> actualSer = createSerializationSchema(SCHEMA, tableOptions); assertThat(actualSer).isEqualTo(expectedSer); } @@ -194,12 +207,19 @@ class RawFormatFactoryTest { variantRowType.getTypeAt(0), InternalTypeInfo.of(variantRowType), UTF_8.name(), - true); + true, + null, + VariantEncoding.JSON); assertThat(createDeserializationSchema(variantSchema, tableOptions)) .isEqualTo(expectedDeser); final RawFormatSerializationSchema expectedSer = - new RawFormatSerializationSchema(variantRowType.getTypeAt(0), UTF_8.name(), true); + new RawFormatSerializationSchema( + variantRowType.getTypeAt(0), + UTF_8.name(), + true, + null, + VariantEncoding.JSON); assertThat(createSerializationSchema(variantSchema, tableOptions)).isEqualTo(expectedSer); } @@ -243,6 +263,41 @@ class RawFormatFactoryTest { return Stream.of(DataTypes.VARIANT(), DataTypes.BYTES(), DataTypes.INT()); } + @Test + void testVariantBinaryEncoding() { + final ResolvedSchema variantSchema = + ResolvedSchema.of(Column.physical("field1", DataTypes.VARIANT())); + final RowType variantRowType = + (RowType) variantSchema.toPhysicalRowDataType().getLogicalType(); + // The charset does not apply to the binary encoding, so UTF-16 is accepted. + final Map<String, String> tableOptions = + getModifiedOptions( + options -> { + options.put("raw.variant-encoding", "binary"); + options.put("raw.charset", UTF_16.name()); + }); + + final RawFormatDeserializationSchema expectedDeser = + new RawFormatDeserializationSchema( + variantRowType.getTypeAt(0), + InternalTypeInfo.of(variantRowType), + UTF_16.name(), + true, + null, + VariantEncoding.BINARY); + assertThat(createDeserializationSchema(variantSchema, tableOptions)) + .isEqualTo(expectedDeser); + + final RawFormatSerializationSchema expectedSer = + new RawFormatSerializationSchema( + variantRowType.getTypeAt(0), + UTF_16.name(), + true, + null, + VariantEncoding.BINARY); + assertThat(createSerializationSchema(variantSchema, tableOptions)).isEqualTo(expectedSer); + } + @Test void testLineDelimiterOption() { final Map<String, String> tableOptions = @@ -258,14 +313,16 @@ class RawFormatFactoryTest { InternalTypeInfo.of(ROW_TYPE), UTF_8.name(), true, - "\n"); + "\n", + VariantEncoding.JSON); DeserializationSchema<RowData> actualDeser = createDeserializationSchema(SCHEMA, tableOptions); assertThat(actualDeser).isEqualTo(expectedDeser); // test serialization schema contains line delimiter final RawFormatSerializationSchema expectedSer = - new RawFormatSerializationSchema(ROW_TYPE.getTypeAt(0), UTF_8.name(), true, "\n"); + new RawFormatSerializationSchema( + ROW_TYPE.getTypeAt(0), UTF_8.name(), true, "\n", VariantEncoding.JSON); SerializationSchema<RowData> actualSer = createSerializationSchema(SCHEMA, tableOptions); assertThat(actualSer).isEqualTo(expectedSer); } diff --git a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatLineDelimiterTest.java b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatLineDelimiterTest.java index 0c138ab33e1..d08e9137c8b 100644 --- a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatLineDelimiterTest.java +++ b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatLineDelimiterTest.java @@ -22,6 +22,7 @@ import org.apache.flink.api.common.serialization.DeserializationSchema; import org.apache.flink.api.common.serialization.SerializationSchema; import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.formats.raw.RawFormatDeserializationSchema; +import org.apache.flink.formats.raw.RawFormatOptions.VariantEncoding; import org.apache.flink.formats.raw.RawFormatSerializationSchema; import org.apache.flink.metrics.MetricGroup; import org.apache.flink.table.data.GenericRowData; @@ -68,7 +69,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - null); + null, + VariantEncoding.JSON); openDeser(schema); List<RowData> rows = collectRows(schema, "hello".getBytes(StandardCharsets.UTF_8)); @@ -86,7 +88,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - "\n")); + "\n", + VariantEncoding.JSON)); openDeser(schema); byte[] message = "line1\nline2\nline3".getBytes(StandardCharsets.UTF_8); @@ -105,7 +108,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - "||"); + "||", + VariantEncoding.JSON); openDeser(schema); byte[] message = "record1||record2||record3".getBytes(StandardCharsets.UTF_8); @@ -124,7 +128,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - "\n"); + "\n", + VariantEncoding.JSON); openDeser(schema); List<RowData> rows = collectRows(schema, null); @@ -143,7 +148,12 @@ class RawFormatLineDelimiterTest { RawFormatDeserializationSchema schema = InstantiationUtil.clone( new RawFormatDeserializationSchema( - STRING_TYPE, TypeInformation.of(RowData.class), "GBK", true, "|")); + STRING_TYPE, + TypeInformation.of(RowData.class), + "GBK", + true, + "|", + VariantEncoding.JSON)); openDeser(schema); List<RowData> rows = collectRows(schema, message); @@ -160,7 +170,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - "\n"); + "\n", + VariantEncoding.JSON); openDeser(schema); byte[] message = {'a', (byte) 0xFF, '\n', (byte) 0x80}; @@ -177,7 +188,11 @@ class RawFormatLineDelimiterTest { void testSerializeWithoutDelimiter_noAppend() throws Exception { RawFormatSerializationSchema schema = new RawFormatSerializationSchema( - STRING_TYPE, StandardCharsets.UTF_8.name(), true, null); + STRING_TYPE, + StandardCharsets.UTF_8.name(), + true, + null, + VariantEncoding.JSON); openSer(schema); RowData row = buildStringRow("hello"); @@ -189,7 +204,11 @@ class RawFormatLineDelimiterTest { void testSerializeWithNewlineDelimiter_appendsDelimiter() throws Exception { RawFormatSerializationSchema schema = new RawFormatSerializationSchema( - STRING_TYPE, StandardCharsets.UTF_8.name(), true, "\n"); + STRING_TYPE, + StandardCharsets.UTF_8.name(), + true, + "\n", + VariantEncoding.JSON); openSer(schema); RowData row = buildStringRow("hello"); @@ -201,7 +220,11 @@ class RawFormatLineDelimiterTest { void testSerializeWithCustomDelimiter_appendsDelimiter() throws Exception { RawFormatSerializationSchema schema = new RawFormatSerializationSchema( - STRING_TYPE, StandardCharsets.UTF_8.name(), true, "||"); + STRING_TYPE, + StandardCharsets.UTF_8.name(), + true, + "||", + VariantEncoding.JSON); openSer(schema); RowData row = buildStringRow("record1"); @@ -213,7 +236,11 @@ class RawFormatLineDelimiterTest { void testSerializeNullRow_returnsNull() throws Exception { RawFormatSerializationSchema schema = new RawFormatSerializationSchema( - STRING_TYPE, StandardCharsets.UTF_8.name(), true, "\n"); + STRING_TYPE, + StandardCharsets.UTF_8.name(), + true, + "\n", + VariantEncoding.JSON); openSer(schema); GenericRowData nullRow = new GenericRowData(1); @@ -233,7 +260,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - "\n"); + "\n", + VariantEncoding.JSON); openDeser(schema); // Message already ends with the delimiter (as produced by the serializer) @@ -249,7 +277,11 @@ class RawFormatLineDelimiterTest { // deserializer when both share the same delimiter configuration. RawFormatSerializationSchema ser = new RawFormatSerializationSchema( - STRING_TYPE, StandardCharsets.UTF_8.name(), true, "\n"); + STRING_TYPE, + StandardCharsets.UTF_8.name(), + true, + "\n", + VariantEncoding.JSON); openSer(ser); RawFormatDeserializationSchema deser = @@ -258,7 +290,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - "\n"); + "\n", + VariantEncoding.JSON); openDeser(deser); // Serialize a single row -> "hello\n" @@ -278,7 +311,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - "é"); + "é", + VariantEncoding.JSON); openDeser(schema); // The delimiter é is c3 a9 and the data holds ü, which is c3 bc, so a match on the first @@ -297,7 +331,8 @@ class RawFormatLineDelimiterTest { TypeInformation.of(RowData.class), StandardCharsets.UTF_8.name(), true, - null); + null, + VariantEncoding.JSON); openDeser(deser); byte[] message = {'{', '"', 'a', '"', ':', '"', (byte) 0xFF, '"', '}'}; diff --git a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatSerDeSchemaTest.java b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatSerDeSchemaTest.java index 1f38c570af4..0ad9c5ba49b 100644 --- a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatSerDeSchemaTest.java +++ b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/formats/raw/RawFormatSerDeSchemaTest.java @@ -24,22 +24,28 @@ import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.api.common.typeutils.base.LocalDateTimeSerializer; import org.apache.flink.core.memory.DataOutputSerializer; import org.apache.flink.formats.raw.RawFormatDeserializationSchema; +import org.apache.flink.formats.raw.RawFormatOptions.VariantEncoding; import org.apache.flink.formats.raw.RawFormatSerializationSchema; import org.apache.flink.table.data.RowData; import org.apache.flink.table.data.conversion.DataStructureConverter; import org.apache.flink.table.data.conversion.DataStructureConverters; import org.apache.flink.table.types.DataType; +import org.apache.flink.types.DeserializationException; import org.apache.flink.types.Row; import org.apache.flink.types.variant.BinaryVariantInternalBuilder; import org.apache.flink.types.variant.Variant; +import org.apache.flink.types.variant.VariantBuilder; +import org.apache.flink.types.variant.VariantTypeException; import org.apache.flink.util.InstantiationUtil; import org.apache.flink.util.StringUtils; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -62,6 +68,7 @@ import static org.apache.flink.table.api.DataTypes.VARCHAR; import static org.apache.flink.table.api.DataTypes.VARIANT; import static org.apache.flink.util.StringUtils.hexStringToByte; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; /** Tests for {@link RawFormatDeserializationSchema} {@link RawFormatSerializationSchema}. */ @@ -70,6 +77,8 @@ class RawFormatSerDeSchemaTest { private static final String JSON_OBJECT = "{\"a\":1,\"b\":\"x\",\"c\":[1,2,3],\"d\":null,\"e\":true}"; + private static final VariantBuilder VARIANT_BUILDER = Variant.newBuilder(); + static List<TestSpec> testData() { return Arrays.asList( TestSpec.type(TINYINT()).values(Byte.MAX_VALUE).binary(new byte[] {Byte.MAX_VALUE}), @@ -164,7 +173,29 @@ class RawFormatSerDeSchemaTest { TestSpec.type(RAW(LocalDateTime.class, new LocalDateTimeSerializer())) .values((Object) null) .binary((byte[]) null), - TestSpec.type(VARIANT()).values((Object) null).binary((byte[]) null)); + TestSpec.type(VARIANT()).values((Object) null).binary((byte[]) null), + + // test variants in the binary encoding, the metadata followed by the value + TestSpec.type(VARIANT()) + .values(VARIANT_BUILDER.of((byte) 7)) + .withVariantEncoding(VariantEncoding.BINARY) + .binary(hexStringToByte("0100000c07")), + // {"d": DATE 2026-11-03, "b": BYTES x'04'}, two types JSON cannot represent + TestSpec.type(VARIANT()) + .values( + VARIANT_BUILDER + .object() + .add("d", VARIANT_BUILDER.of(LocalDate.parse("2026-11-03"))) + .add("b", VARIANT_BUILDER.of(new byte[] {4})) + .build()) + .withVariantEncoding(VariantEncoding.BINARY) + .binary( + hexStringToByte( + "01020001026462" + "0202010005000b2c185100003c0100000004")), + TestSpec.type(VARIANT()) + .values((Object) null) + .withVariantEncoding(VariantEncoding.BINARY) + .binary((byte[]) null)); } @ParameterizedTest @@ -178,13 +209,17 @@ class RawFormatSerDeSchemaTest { testSpec.type.getLogicalType(), TypeInformation.of(RowData.class), testSpec.charsetName, - testSpec.isBigEndian)); + testSpec.isBigEndian, + null, + testSpec.variantEncoding)); RawFormatSerializationSchema serializationSchema = InstantiationUtil.clone( new RawFormatSerializationSchema( testSpec.type.getLogicalType(), testSpec.charsetName, - testSpec.isBigEndian)); + testSpec.isBigEndian, + null, + testSpec.variantEncoding)); deserializationSchema.open(mock(DeserializationSchema.InitializationContext.class)); serializationSchema.open(mock(SerializationSchema.InitializationContext.class)); @@ -219,6 +254,45 @@ class RawFormatSerDeSchemaTest { } } + @Test + void testDeserializeMalformedVariantBinary() throws Exception { + final RawFormatDeserializationSchema schema = createBinaryVariantDeserializationSchema(); + + // A dictionary of one key without its offsets. + assertThatThrownBy(() -> schema.deserialize(hexStringToByte("0101"))) + .isInstanceOf(DeserializationException.class) + .hasMessage( + "Failed to deserialize VARIANT type. " + + "The received data is not a valid VARIANT binary encoding.") + .cause() + .hasMessage("MALFORMED_VARIANT"); + } + + @Test + void testDeserializeVariantBinaryChecksTheValueWhenItIsRead() throws Exception { + final RawFormatDeserializationSchema schema = createBinaryVariantDeserializationSchema(); + + // Complete metadata without a value is accepted, and fails once the value is read. + final Variant variant = schema.deserialize(hexStringToByte("010000")).getVariant(0); + assertThatThrownBy(variant::getType) + .isInstanceOf(VariantTypeException.class) + .hasMessage("MALFORMED_VARIANT"); + } + + private static RawFormatDeserializationSchema createBinaryVariantDeserializationSchema() + throws Exception { + final RawFormatDeserializationSchema schema = + new RawFormatDeserializationSchema( + VARIANT().getLogicalType(), + TypeInformation.of(RowData.class), + StandardCharsets.UTF_8.name(), + true, + null, + VariantEncoding.BINARY); + schema.open(mock(DeserializationSchema.InitializationContext.class)); + return schema; + } + private static Variant variant(String json) { try { return BinaryVariantInternalBuilder.parseJson(json, false); @@ -247,6 +321,7 @@ class RawFormatSerDeSchemaTest { private final DataType type; private String charsetName = "UTF-8"; private boolean isBigEndian = true; + private VariantEncoding variantEncoding = VariantEncoding.JSON; private TestSpec(DataType type) { this.type = type; @@ -271,6 +346,11 @@ class RawFormatSerDeSchemaTest { return this; } + public TestSpec withVariantEncoding(VariantEncoding variantEncoding) { + this.variantEncoding = variantEncoding; + return this; + } + public TestSpec withLittleEndian() { this.isBigEndian = false; return this; @@ -295,6 +375,8 @@ class RawFormatSerDeSchemaTest { + '\'' + ", isBigEndian=" + isBigEndian + + ", variantEncoding=" + + variantEncoding + '}'; } }
