blob: 30892173e416e149440d379dd8089452e7d46a2d [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import org.apache.parquet.variant.ImmutableMetadata;
import org.apache.parquet.variant.Variant;
import org.apache.parquet.variant.VariantArrayBuilder;
import org.apache.parquet.variant.VariantBuilder;
import org.apache.parquet.variant.VariantObjectBuilder;
/** Offline generator and verifier for the VariantEncoding v1 golden corpus. */
public final class ParquetVariantGolden {
private static final String ARTIFACT = "org.apache.parquet:parquet-variant:1.17.0";
private static final String JAR_SHA256 =
"daecf8161e7bba63f7ba9fd62c1e8a77730c9a9d76a335191dc9d0a0fcaaec52";
private static final HexFormat HEX = HexFormat.of();
private static final Comparator<String> UNSIGNED_UTF8 =
(left, right) -> Arrays.compareUnsigned(utf8(left), utf8(right));
private ParquetVariantGolden() {}
@FunctionalInterface
private interface Appender {
void append(VariantBuilder builder);
}
private record GoldenVector(
String name,
String provenance,
String rootType,
String expected,
byte[] metadata,
byte[] value) {}
public static void main(String[] args) throws Exception {
verifyArtifact();
if (args.length == 2 && args[0].equals("generate")) {
knownIncompatibilityCheck();
Path output = Path.of(args[1]);
Files.createDirectories(output);
writeCorpus(
output.resolve("parquet_java_vectors.tsv"),
"java-to-doris",
javaToDorisVectors());
writeCorpus(
output.resolve("doris_java_verified_vectors.tsv"),
"doris-to-java",
dorisToJavaVectors());
return;
}
if (args.length == 1 && args[0].equals("extended")) {
knownIncompatibilityCheck();
verifyRealFourByteBoundaries();
return;
}
throw new IllegalArgumentException(
"Usage: ParquetVariantGolden generate <output-dir> | extended");
}
private static List<GoldenVector> javaToDorisVectors() {
List<GoldenVector> vectors = new ArrayList<>();
vectors.add(builderVector("primitive_null", "null", VariantBuilder::appendNull));
vectors.add(builderVector("primitive_true", "bool:true", b -> b.appendBoolean(true)));
vectors.add(builderVector("primitive_false", "bool:false", b -> b.appendBoolean(false)));
vectors.add(builderVector("primitive_int8", "int8:-7", b -> b.appendByte((byte) -7)));
vectors.add(builderVector("primitive_int16", "int16:128", b -> b.appendShort((short) 128)));
vectors.add(builderVector("primitive_int32", "int32:32768", b -> b.appendInt(32768)));
vectors.add(
builderVector(
"primitive_int64", "int64:2147483648", b -> b.appendLong(2147483648L)));
vectors.add(
builderVector(
"primitive_int8_min", "int8:-128", b -> b.appendByte(Byte.MIN_VALUE)));
vectors.add(
builderVector(
"primitive_int8_max", "int8:127", b -> b.appendByte(Byte.MAX_VALUE)));
vectors.add(
builderVector(
"primitive_int16_min",
"int16:-32768",
b -> b.appendShort(Short.MIN_VALUE)));
vectors.add(
builderVector(
"primitive_int16_max",
"int16:32767",
b -> b.appendShort(Short.MAX_VALUE)));
vectors.add(
builderVector(
"primitive_int32_min",
"int32:-2147483648",
b -> b.appendInt(Integer.MIN_VALUE)));
vectors.add(
builderVector(
"primitive_int32_max",
"int32:2147483647",
b -> b.appendInt(Integer.MAX_VALUE)));
vectors.add(
builderVector(
"primitive_int64_min",
"int64:-9223372036854775808",
b -> b.appendLong(Long.MIN_VALUE)));
vectors.add(
builderVector(
"primitive_int64_max",
"int64:9223372036854775807",
b -> b.appendLong(Long.MAX_VALUE)));
vectors.add(
builderVector(
"primitive_double",
"double:400921fb54442d18",
b -> b.appendDouble(Math.PI)));
vectors.add(
builderVector(
"primitive_decimal4",
"decimal4:1234567:2",
b -> b.appendDecimal(new BigDecimal("12345.67"))));
vectors.add(
builderVector(
"primitive_decimal8",
"decimal8:123456789012345:5",
b -> b.appendDecimal(new BigDecimal("1234567890.12345"))));
vectors.add(
builderVector(
"primitive_decimal16",
"decimal16:12345678901234567891234567890:10",
b -> b.appendDecimal(new BigDecimal("1234567890123456789.1234567890"))));
vectors.add(
builderVector(
"primitive_decimal4_precision9_max",
"decimal4:999999999:2",
b -> b.appendDecimal(new BigDecimal("9999999.99"))));
vectors.add(
builderVector(
"primitive_decimal4_precision9_min",
"decimal4:-999999999:2",
b -> b.appendDecimal(new BigDecimal("-9999999.99"))));
vectors.add(
builderVector(
"primitive_decimal8_precision10_min",
"decimal8:1000000000:0",
b -> b.appendDecimal(new BigDecimal("1000000000"))));
vectors.add(
builderVector(
"primitive_decimal8_precision18_max",
"decimal8:999999999999999999:0",
b -> b.appendDecimal(new BigDecimal("999999999999999999"))));
vectors.add(
builderVector(
"primitive_decimal16_precision19_min",
"decimal16:1000000000000000000:0",
b -> b.appendDecimal(new BigDecimal("1000000000000000000"))));
vectors.add(
builderVector(
"primitive_decimal16_precision38_max",
"decimal16:99999999999999999999999999999999999999:0",
b ->
b.appendDecimal(
new BigDecimal(
"99999999999999999999999999999999999999"))));
vectors.add(
builderVector(
"primitive_decimal4_scale38_min_unit",
"decimal4:-1:38",
b ->
b.appendDecimal(
new BigDecimal(
"-0.00000000000000000000000000000000000001"))));
vectors.add(
builderVector(
"primitive_decimal4_trailing_zero",
"decimal4:12300:4",
b -> b.appendDecimal(new BigDecimal("1.2300"))));
vectors.add(builderVector("primitive_date", "date:-12345", b -> b.appendDate(-12345)));
vectors.add(
builderVector(
"primitive_timestamp_tz",
"timestamp_tz:-1234567890123",
b -> b.appendTimestampTz(-1234567890123L)));
vectors.add(
builderVector(
"primitive_timestamp_ntz",
"timestamp_ntz:2234567890123",
b -> b.appendTimestampNtz(2234567890123L)));
vectors.add(
builderVector(
"primitive_float", "float:3fc00000", b -> b.appendFloat(1.5F)));
vectors.add(
builderVector(
"primitive_binary",
"binary:0001ff",
b -> b.appendBinary(ByteBuffer.wrap(new byte[] {0, 1, (byte) 0xFF}))));
String longString = "L".repeat(64);
vectors.add(
builderVector(
"primitive_long_string",
"string:" + hexUtf8(longString),
b -> b.appendString(longString)));
vectors.add(
builderVector(
"primitive_time",
"time:86399999999",
b -> b.appendTime(86_399_999_999L)));
vectors.add(
builderVector(
"primitive_timestamp_nanos_tz",
"timestamp_nanos_tz:-3234567890123",
b -> b.appendTimestampNanosTz(-3234567890123L)));
vectors.add(
builderVector(
"primitive_timestamp_nanos_ntz",
"timestamp_nanos_ntz:4234567890123",
b -> b.appendTimestampNanosNtz(4234567890123L)));
UUID uuid = UUID.fromString("00112233-4455-6677-8899-aabbccddeeff");
vectors.add(
builderVector(
"primitive_uuid", "uuid:" + uuid, b -> b.appendUUID(uuid)));
vectors.add(builderVector("short_string_empty", "string:", b -> b.appendString("")));
String short63 = "s".repeat(63);
vectors.add(
builderVector(
"short_string_63",
"string:" + hexUtf8(short63),
b -> b.appendString(short63)));
String unicode = "A\u00E9\u4E2D\uD800\uDC00";
vectors.add(
builderVector(
"short_string_unicode",
"string:" + hexUtf8(unicode),
b -> b.appendString(unicode)));
vectors.add(
builderVector(
"empty_object",
"object{}",
builder -> {
builder.startObject();
builder.endObject();
}));
vectors.add(
builderVector(
"empty_array",
"array[]",
builder -> {
builder.startArray();
builder.endArray();
}));
vectors.add(normalObjectVector());
vectors.add(normalArrayVector());
vectors.add(normalNestedVector());
vectors.add(unicodeBmpObjectVector());
vectors.add(unicodeSupplementaryObjectVector());
vectors.add(metadataBoundaryVector(255));
vectors.add(metadataBoundaryVector(256));
vectors.add(metadataBoundaryVector(65_535));
vectors.add(metadataBoundaryVector(65_536));
vectors.add(offsetBoundaryVector(255));
vectors.add(offsetBoundaryVector(256));
vectors.add(offsetBoundaryVector(65_535));
vectors.add(offsetBoundaryVector(65_536));
vectors.add(arrayCountBoundaryVector(255));
vectors.add(arrayCountBoundaryVector(256));
vectors.add(objectIdBoundaryVector(256));
vectors.add(objectIdBoundaryVector(257));
vectors.add(rawMetadataWidthFourVector());
vectors.add(rawArrayOffsetWidthFourVector());
vectors.add(rawObjectIdWidthVector(3));
vectors.add(rawObjectIdWidthVector(4));
vectors.add(rawNonMonotonicObjectVector());
return vectors;
}
private static List<GoldenVector> dorisToJavaVectors() {
List<GoldenVector> vectors = new ArrayList<>();
vectors.add(canonicalVector("doris_null", "null", List.of(), VariantBuilder::appendNull));
vectors.add(
canonicalVector(
"doris_true", "bool:true", List.of(), b -> b.appendBoolean(true)));
vectors.add(
canonicalVector(
"doris_false", "bool:false", List.of(), b -> b.appendBoolean(false)));
vectors.add(
canonicalVector(
"doris_int8", "int8:-7", List.of(), b -> b.appendByte((byte) -7)));
vectors.add(
canonicalVector(
"doris_int16", "int16:128", List.of(), b -> b.appendShort((short) 128)));
vectors.add(
canonicalVector(
"doris_int32", "int32:32768", List.of(), b -> b.appendInt(32768)));
vectors.add(
canonicalVector(
"doris_int64",
"int64:2147483648",
List.of(),
b -> b.appendLong(2147483648L)));
vectors.add(
canonicalVector(
"doris_int8_min",
"int8:-128",
List.of(),
b -> b.appendByte(Byte.MIN_VALUE)));
vectors.add(
canonicalVector(
"doris_int8_max",
"int8:127",
List.of(),
b -> b.appendByte(Byte.MAX_VALUE)));
vectors.add(
canonicalVector(
"doris_int16_min",
"int16:-32768",
List.of(),
b -> b.appendShort(Short.MIN_VALUE)));
vectors.add(
canonicalVector(
"doris_int16_max",
"int16:32767",
List.of(),
b -> b.appendShort(Short.MAX_VALUE)));
vectors.add(
canonicalVector(
"doris_int32_min",
"int32:-2147483648",
List.of(),
b -> b.appendInt(Integer.MIN_VALUE)));
vectors.add(
canonicalVector(
"doris_int32_max",
"int32:2147483647",
List.of(),
b -> b.appendInt(Integer.MAX_VALUE)));
vectors.add(
canonicalVector(
"doris_int64_min",
"int64:-9223372036854775808",
List.of(),
b -> b.appendLong(Long.MIN_VALUE)));
vectors.add(
canonicalVector(
"doris_int64_max",
"int64:9223372036854775807",
List.of(),
b -> b.appendLong(Long.MAX_VALUE)));
vectors.add(
canonicalVector(
"doris_double",
"double:400921fb54442d18",
List.of(),
b -> b.appendDouble(Math.PI)));
vectors.add(
canonicalVector(
"doris_decimal4",
"decimal4:1234567:2",
List.of(),
b -> b.appendDecimal(new BigDecimal("12345.67"))));
vectors.add(
canonicalVector(
"doris_decimal8",
"decimal8:123456789012345:5",
List.of(),
b -> b.appendDecimal(new BigDecimal("1234567890.12345"))));
vectors.add(
canonicalVector(
"doris_decimal16",
"decimal16:12345678901234567891234567890:10",
List.of(),
b -> b.appendDecimal(new BigDecimal("1234567890123456789.1234567890"))));
vectors.add(
canonicalVector(
"doris_decimal4_precision9_max",
"decimal4:999999999:2",
List.of(),
b -> b.appendDecimal(new BigDecimal("9999999.99"))));
vectors.add(
canonicalVector(
"doris_decimal4_precision9_min",
"decimal4:-999999999:2",
List.of(),
b -> b.appendDecimal(new BigDecimal("-9999999.99"))));
vectors.add(
canonicalVector(
"doris_decimal8_precision10_min",
"decimal8:1000000000:0",
List.of(),
b -> b.appendDecimal(new BigDecimal("1000000000"))));
vectors.add(
canonicalVector(
"doris_decimal8_precision18_max",
"decimal8:999999999999999999:0",
List.of(),
b -> b.appendDecimal(new BigDecimal("999999999999999999"))));
vectors.add(
canonicalVector(
"doris_decimal16_precision19_min",
"decimal16:1000000000000000000:0",
List.of(),
b -> b.appendDecimal(new BigDecimal("1000000000000000000"))));
vectors.add(
canonicalVector(
"doris_decimal16_precision38_max",
"decimal16:99999999999999999999999999999999999999:0",
List.of(),
b ->
b.appendDecimal(
new BigDecimal(
"99999999999999999999999999999999999999"))));
vectors.add(
canonicalVector(
"doris_decimal4_scale38_min_unit",
"decimal4:-1:38",
List.of(),
b ->
b.appendDecimal(
new BigDecimal(
"-0.00000000000000000000000000000000000001"))));
vectors.add(
canonicalVector(
"doris_decimal4_trailing_zero",
"decimal4:12300:4",
List.of(),
b -> b.appendDecimal(new BigDecimal("1.2300"))));
vectors.add(
canonicalVector(
"doris_date", "date:-12345", List.of(), b -> b.appendDate(-12345)));
vectors.add(
canonicalVector(
"doris_timestamp_tz",
"timestamp_tz:-1234567890123",
List.of(),
b -> b.appendTimestampTz(-1234567890123L)));
vectors.add(
canonicalVector(
"doris_timestamp_ntz",
"timestamp_ntz:2234567890123",
List.of(),
b -> b.appendTimestampNtz(2234567890123L)));
vectors.add(
canonicalVector(
"doris_float",
"float:3fc00000",
List.of(),
b -> b.appendFloat(1.5F)));
vectors.add(
canonicalVector(
"doris_binary",
"binary:0001ff",
List.of(),
b -> b.appendBinary(ByteBuffer.wrap(new byte[] {0, 1, (byte) 0xFF}))));
String longString = "L".repeat(64);
vectors.add(
canonicalVector(
"doris_long_string",
"string:" + hexUtf8(longString),
List.of(),
b -> b.appendString(longString)));
vectors.add(
canonicalVector(
"doris_time",
"time:86399999999",
List.of(),
b -> b.appendTime(86_399_999_999L)));
vectors.add(
canonicalVector(
"doris_timestamp_nanos_tz",
"timestamp_nanos_tz:-3234567890123",
List.of(),
b -> b.appendTimestampNanosTz(-3234567890123L)));
vectors.add(
canonicalVector(
"doris_timestamp_nanos_ntz",
"timestamp_nanos_ntz:4234567890123",
List.of(),
b -> b.appendTimestampNanosNtz(4234567890123L)));
UUID uuid = UUID.fromString("00112233-4455-6677-8899-aabbccddeeff");
vectors.add(
canonicalVector(
"doris_uuid", "uuid:" + uuid, List.of(), b -> b.appendUUID(uuid)));
vectors.add(
canonicalVector(
"doris_short_empty", "string:", List.of(), b -> b.appendString("")));
String short63 = "s".repeat(63);
vectors.add(
canonicalVector(
"doris_short_63",
"string:" + hexUtf8(short63),
List.of(),
b -> b.appendString(short63)));
String unicode = "A\u00E9\u4E2D\uD800\uDC00";
vectors.add(
canonicalVector(
"doris_unicode_string",
"string:" + hexUtf8(unicode),
List.of(),
b -> b.appendString(unicode)));
vectors.add(canonicalObjectVector());
vectors.add(canonicalArrayVector());
vectors.add(canonicalNestedVector());
vectors.add(canonicalUnicodeObjectVector());
return vectors;
}
private static GoldenVector builderVector(String name, String expected, Appender append) {
VariantBuilder builder = new VariantBuilder();
append.append(builder);
GoldenVector vector =
checkedVector(name, "parquet-java-builder", expected, builder.build());
if ((vector.metadata()[0] & 0x10) != 0) {
throw new IllegalStateException("MetadataBuilder unexpectedly set sorted_strings");
}
return vector;
}
private static void verifyArtifact() throws Exception {
Path artifact =
Path.of(
Variant.class
.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI());
if (!Files.isRegularFile(artifact)) {
throw new IllegalStateException(
"parquet-java Variant classes were not loaded from a jar: " + artifact);
}
byte[] digest =
MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(artifact));
String actual = HEX.formatHex(digest);
if (!actual.equals(JAR_SHA256)) {
throw new IllegalStateException(
"parquet-java jar SHA-256 mismatch: expected "
+ JAR_SHA256
+ ", got "
+ actual
+ " from "
+ artifact);
}
}
private static GoldenVector canonicalVector(
String name, String expected, List<String> keys, Appender append) {
List<String> sortedKeys = new ArrayList<>(keys);
sortedKeys.sort(UNSIGNED_UTF8);
if (new HashSet<>(sortedKeys).size() != sortedKeys.size()) {
throw new IllegalArgumentException("Duplicate canonical metadata key for " + name);
}
byte[] encodedMetadata = encodeMetadata(sortedKeys, true, 0);
ImmutableMetadata metadata = new ImmutableMetadata(ByteBuffer.wrap(encodedMetadata));
for (int index = 0; index < sortedKeys.size(); ++index) {
if (metadata.getOrInsert(sortedKeys.get(index)) != index) {
throw new IllegalStateException("Immutable metadata id mismatch for " + name);
}
}
VariantBuilder builder = new VariantBuilder(metadata);
append.append(builder);
Variant variant = builder.build();
if (!Arrays.equals(encodedMetadata, bytes(variant.getMetadataBuffer()))) {
throw new IllegalStateException("Canonical metadata changed for " + name);
}
return checkedVector(name, "doris-block-builder-java-validated", expected, variant);
}
private static GoldenVector checkedVector(
String name, String provenance, String expected, Variant variant) {
String actual = describe(variant);
if (!actual.equals(expected)) {
throw new IllegalStateException(
"Typed accessor mismatch for " + name + ": expected " + expected + ", got " + actual);
}
return new GoldenVector(
name,
provenance,
rootType(variant),
expected,
bytes(variant.getMetadataBuffer()),
bytes(variant.getValueBuffer()));
}
private static GoldenVector checkedRawVector(
String name, String expected, byte[] metadata, byte[] value) {
ImmutableMetadata parsed = new ImmutableMetadata(ByteBuffer.wrap(metadata));
if (!Arrays.equals(metadata, bytes(parsed.getEncodedBuffer()))) {
throw new IllegalStateException("ImmutableMetadata changed raw bytes for " + name);
}
return checkedVector(
name,
"spec-raw-java-validated",
expected,
new Variant(value, metadata));
}
private static GoldenVector normalObjectVector() {
String expected = "object{61=string:78;7a=int32:1}";
return builderVector(
"object_unsorted_metadata",
expected,
builder -> {
VariantObjectBuilder object = builder.startObject();
object.appendKey("z");
object.appendInt(1);
object.appendKey("a");
object.appendString("x");
builder.endObject();
});
}
private static GoldenVector normalArrayVector() {
return builderVector(
"array_ordered",
"array[int8:1;string:78;bool:false]",
builder -> {
VariantArrayBuilder array = builder.startArray();
array.appendByte((byte) 1);
array.appendString("x");
array.appendBoolean(false);
builder.endArray();
});
}
private static GoldenVector normalNestedVector() {
String expected = "object{617272=array[int8:7;object{696e73696465=bool:false}]}";
return builderVector(
"nested_object_array",
expected,
builder -> {
VariantObjectBuilder root = builder.startObject();
root.appendKey("arr");
VariantArrayBuilder array = root.startArray();
array.appendByte((byte) 7);
VariantObjectBuilder inner = array.startObject();
inner.appendKey("inside");
inner.appendBoolean(false);
array.endObject();
root.endArray();
builder.endObject();
});
}
private static GoldenVector unicodeBmpObjectVector() {
String first = "\u00E9";
String second = "\uE000";
String expected =
"object{" + hexUtf8(first) + "=int32:1;" + hexUtf8(second) + "=int32:2}";
return builderVector(
"unicode_bmp_order",
expected,
builder -> {
VariantObjectBuilder object = builder.startObject();
object.appendKey(second);
object.appendInt(2);
object.appendKey(first);
object.appendInt(1);
builder.endObject();
});
}
private static GoldenVector unicodeSupplementaryObjectVector() {
String first = "\uD800\uDC00";
String second = "\uD800\uDC01";
String expected =
"object{" + hexUtf8(first) + "=int32:1;" + hexUtf8(second) + "=int32:2}";
return builderVector(
"unicode_supplementary_order",
expected,
builder -> {
VariantObjectBuilder object = builder.startObject();
object.appendKey(second);
object.appendInt(2);
object.appendKey(first);
object.appendInt(1);
builder.endObject();
});
}
private static GoldenVector metadataBoundaryVector(int keyBytes) {
String key = "k".repeat(keyBytes);
GoldenVector vector =
builderVector(
"metadata_bytes_" + keyBytes,
"object{" + hexUtf8(key) + "=null}",
builder -> {
VariantObjectBuilder object = builder.startObject();
object.appendKey(key);
object.appendNull();
builder.endObject();
});
int expectedWidth = minUnsignedWidth(keyBytes);
int actualWidth = ((vector.metadata()[0] >>> 6) & 0x03) + 1;
if (actualWidth != expectedWidth) {
throw new IllegalStateException("Metadata width boundary mismatch at " + keyBytes);
}
return vector;
}
private static GoldenVector offsetBoundaryVector(int encodedChildBytes) {
byte[] payload = new byte[encodedChildBytes - 5];
Arrays.fill(payload, (byte) 0xAB);
GoldenVector vector =
builderVector(
"array_offset_bytes_" + encodedChildBytes,
"array[binary:" + HEX.formatHex(payload) + "]",
builder -> {
VariantArrayBuilder array = builder.startArray();
array.appendBinary(ByteBuffer.wrap(payload));
builder.endArray();
});
int valueHeader = (vector.value()[0] & 0xFF) >>> 2;
int actualWidth = (valueHeader & 0x03) + 1;
if (actualWidth != minUnsignedWidth(encodedChildBytes)) {
throw new IllegalStateException(
"Array offset width boundary mismatch at " + encodedChildBytes);
}
return vector;
}
private static GoldenVector arrayCountBoundaryVector(int count) {
GoldenVector vector =
builderVector(
"array_count_" + count,
repeatedArrayExpected(count),
builder -> {
VariantArrayBuilder array = builder.startArray();
for (int index = 0; index < count; ++index) {
array.appendNull();
}
builder.endArray();
});
int valueHeader = (vector.value()[0] & 0xFF) >>> 2;
boolean isLarge = (valueHeader & 0x04) != 0;
if (isLarge != (count > 255)) {
throw new IllegalStateException("Array count boundary mismatch at " + count);
}
return vector;
}
private static GoldenVector objectIdBoundaryVector(int count) {
List<String> keys = numberedKeys(count);
GoldenVector vector =
builderVector(
"object_id_count_" + count,
nullObjectExpected(keys),
builder -> {
VariantObjectBuilder object = builder.startObject();
for (String key : keys) {
object.appendKey(key);
object.appendNull();
}
builder.endObject();
});
int valueHeader = (vector.value()[0] & 0xFF) >>> 2;
int idWidth = ((valueHeader >>> 2) & 0x03) + 1;
boolean isLarge = (valueHeader & 0x10) != 0;
if (idWidth != minUnsignedWidth(count - 1) || isLarge != (count > 255)) {
throw new IllegalStateException("Object id/count boundary mismatch at " + count);
}
return vector;
}
private static GoldenVector rawMetadataWidthFourVector() {
byte[] metadata = encodeMetadata(List.of(), true, 4);
return checkedRawVector(
"raw_metadata_width_4", "null", metadata, new byte[] {0});
}
private static GoldenVector rawArrayOffsetWidthFourVector() {
byte[] metadata = encodeMetadata(List.of(), true, 1);
byte[] value = rawArray(4, List.of(new byte[] {0}));
return checkedRawVector("raw_array_offset_width_4", "array[null]", metadata, value);
}
private static GoldenVector rawObjectIdWidthVector(int idWidth) {
byte[] metadata = encodeMetadata(List.of("a"), true, 1);
byte[] value = rawObject(idWidth, 1, new int[] {0}, new int[] {0, 1}, new byte[] {0});
return checkedRawVector(
"raw_object_id_width_" + idWidth, "object{61=null}", metadata, value);
}
private static GoldenVector rawNonMonotonicObjectVector() {
byte[] metadata = encodeMetadata(List.of("a", "b"), true, 1);
byte[] shortX = shortString("x");
byte[] intOne = new byte[] {(byte) (3 << 2), 1};
byte[] values = concat(shortX, intOne);
byte[] value = rawObject(1, 1, new int[] {0, 1}, new int[] {2, 0, 4}, values);
return checkedRawVector(
"raw_object_nonmonotonic_offsets",
"object{61=int8:1;62=string:78}",
metadata,
value);
}
private static GoldenVector canonicalObjectVector() {
return canonicalVector(
"doris_object",
"object{61=string:78;7a=int8:1}",
List.of("z", "a"),
builder -> {
VariantObjectBuilder object = builder.startObject();
object.appendKey("z");
object.appendByte((byte) 1);
object.appendKey("a");
object.appendString("x");
builder.endObject();
});
}
private static GoldenVector canonicalArrayVector() {
return canonicalVector(
"doris_array",
"array[int8:1;string:78;bool:false]",
List.of(),
builder -> {
VariantArrayBuilder array = builder.startArray();
array.appendByte((byte) 1);
array.appendString("x");
array.appendBoolean(false);
builder.endArray();
});
}
private static GoldenVector canonicalNestedVector() {
return canonicalVector(
"doris_nested",
"object{617272=array[int8:7;object{696e73696465=bool:false}]}",
List.of("arr", "inside"),
builder -> {
VariantObjectBuilder root = builder.startObject();
root.appendKey("arr");
VariantArrayBuilder array = root.startArray();
array.appendByte((byte) 7);
VariantObjectBuilder inner = array.startObject();
inner.appendKey("inside");
inner.appendBoolean(false);
array.endObject();
root.endArray();
builder.endObject();
});
}
private static GoldenVector canonicalUnicodeObjectVector() {
String first = "\u00E9";
String second = "\uE000";
return canonicalVector(
"doris_unicode_object",
"object{" + hexUtf8(first) + "=int8:1;" + hexUtf8(second) + "=int8:2}",
List.of(second, first),
builder -> {
VariantObjectBuilder object = builder.startObject();
object.appendKey(second);
object.appendByte((byte) 2);
object.appendKey(first);
object.appendByte((byte) 1);
builder.endObject();
});
}
private static String describe(Variant variant) {
return switch (variant.getType()) {
case NULL -> "null";
case BOOLEAN -> "bool:" + variant.getBoolean();
case BYTE -> "int8:" + variant.getByte();
case SHORT -> "int16:" + variant.getShort();
case INT -> "int32:" + variant.getInt();
case LONG -> "int64:" + variant.getLong();
case DOUBLE ->
"double:"
+ String.format(
Locale.ROOT,
"%016x",
Double.doubleToRawLongBits(variant.getDouble()));
case DECIMAL4 -> decimalDescription("decimal4", variant.getDecimal());
case DECIMAL8 -> decimalDescription("decimal8", variant.getDecimal());
case DECIMAL16 -> decimalDescription("decimal16", variant.getDecimal());
case DATE -> "date:" + variant.getInt();
case TIMESTAMP_TZ -> "timestamp_tz:" + variant.getLong();
case TIMESTAMP_NTZ -> "timestamp_ntz:" + variant.getLong();
case FLOAT ->
"float:"
+ String.format(
Locale.ROOT,
"%08x",
Float.floatToRawIntBits(variant.getFloat()));
case BINARY -> "binary:" + HEX.formatHex(bytes(variant.getBinary()));
case STRING -> "string:" + hexUtf8(variant.getString());
case TIME -> "time:" + variant.getLong();
case TIMESTAMP_NANOS_TZ -> "timestamp_nanos_tz:" + variant.getLong();
case TIMESTAMP_NANOS_NTZ -> "timestamp_nanos_ntz:" + variant.getLong();
case UUID -> "uuid:" + variant.getUUID().toString().toLowerCase(Locale.ROOT);
case OBJECT -> describeObject(variant);
case ARRAY -> describeArray(variant);
};
}
private static String decimalDescription(String type, BigDecimal decimal) {
return type + ":" + decimal.unscaledValue() + ":" + decimal.scale();
}
private static String describeObject(Variant variant) {
StringBuilder result = new StringBuilder("object{");
for (int index = 0; index < variant.numObjectElements(); ++index) {
if (index != 0) {
result.append(';');
}
Variant.ObjectField field = variant.getFieldAtIndex(index);
Variant byKey = variant.getFieldByKey(field.key);
if (byKey == null || !describe(byKey).equals(describe(field.value))) {
throw new IllegalStateException("Official object lookup disagrees with iteration");
}
result.append(hexUtf8(field.key)).append('=').append(describe(field.value));
}
return result.append('}').toString();
}
private static String describeArray(Variant variant) {
StringBuilder result = new StringBuilder("array[");
for (int index = 0; index < variant.numArrayElements(); ++index) {
if (index != 0) {
result.append(';');
}
result.append(describe(variant.getElementAtIndex(index)));
}
return result.append(']').toString();
}
private static String rootType(Variant variant) {
return switch (variant.getType()) {
case NULL -> "null";
case BOOLEAN -> "boolean";
case BYTE -> "byte";
case SHORT -> "short";
case INT -> "int";
case LONG -> "long";
case STRING -> "string";
case DOUBLE -> "double";
case DECIMAL4 -> "decimal4";
case DECIMAL8 -> "decimal8";
case DECIMAL16 -> "decimal16";
case DATE -> "date";
case TIMESTAMP_TZ -> "timestamp_tz";
case TIMESTAMP_NTZ -> "timestamp_ntz";
case FLOAT -> "float";
case BINARY -> "binary";
case TIME -> "time";
case TIMESTAMP_NANOS_TZ -> "timestamp_nanos_tz";
case TIMESTAMP_NANOS_NTZ -> "timestamp_nanos_ntz";
case UUID -> "uuid";
case OBJECT -> "object";
case ARRAY -> "array";
};
}
private static void writeCorpus(Path file, String corpus, List<GoldenVector> vectors)
throws Exception {
vectors.sort(Comparator.comparing(GoldenVector::name));
Set<String> names = new HashSet<>();
StringBuilder output = new StringBuilder();
output.append("# Licensed to the Apache Software Foundation (ASF) under one\n");
output.append("# or more contributor license agreements. See the NOTICE file\n");
output.append("# distributed with this work for additional information\n");
output.append("# regarding copyright ownership. The ASF licenses this file\n");
output.append("# to you under the Apache License, Version 2.0 (the\n");
output.append("# \"License\"); you may not use this file except in compliance\n");
output.append("# with the License. You may obtain a copy of the License at\n");
output.append("#\n");
output.append("# http://www.apache.org/licenses/LICENSE-2.0\n");
output.append("#\n");
output.append("# Unless required by applicable law or agreed to in writing,\n");
output.append("# software distributed under the License is distributed on an\n");
output.append("# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n");
output.append("# KIND, either express or implied. See the License for the\n");
output.append("# specific language governing permissions and limitations\n");
output.append("# under the License.\n");
output.append("#\n");
output.append("# variant-golden-v1\n");
output.append("# artifact=").append(ARTIFACT).append('\n');
output.append("# jar_sha256=").append(JAR_SHA256).append('\n');
output.append("# corpus=").append(corpus).append('\n');
output.append("# columns=name\\tprovenance\\troot_type\\texpected\\tmetadata_hex\\tvalue_hex\n");
for (GoldenVector vector : vectors) {
if (!names.add(vector.name())) {
throw new IllegalStateException("Duplicate vector name " + vector.name());
}
output.append(vector.name())
.append('\t')
.append(vector.provenance())
.append('\t')
.append(vector.rootType())
.append('\t')
.append(vector.expected())
.append('\t')
.append(HEX.formatHex(vector.metadata()))
.append('\t')
.append(HEX.formatHex(vector.value()))
.append('\n');
}
Files.writeString(file, output, StandardCharsets.UTF_8);
}
private static byte[] encodeMetadata(List<String> keys, boolean sorted, int forcedWidth) {
List<byte[]> encodedKeys = keys.stream().map(ParquetVariantGolden::utf8).toList();
long stringsSize = encodedKeys.stream().mapToLong(bytes -> bytes.length).sum();
int width =
forcedWidth == 0
? minUnsignedWidth(Math.max(keys.size(), stringsSize))
: forcedWidth;
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(1 | (sorted ? 0x10 : 0) | ((width - 1) << 6));
writeUnsigned(output, keys.size(), width);
long offset = 0;
writeUnsigned(output, offset, width);
for (byte[] key : encodedKeys) {
offset += key.length;
writeUnsigned(output, offset, width);
}
for (byte[] key : encodedKeys) {
output.writeBytes(key);
}
return output.toByteArray();
}
private static byte[] rawArray(int offsetWidth, List<byte[]> children) {
ByteArrayOutputStream values = new ByteArrayOutputStream();
int[] offsets = new int[children.size() + 1];
for (int index = 0; index < children.size(); ++index) {
offsets[index] = values.size();
values.writeBytes(children.get(index));
}
offsets[children.size()] = values.size();
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(3 | ((offsetWidth - 1) << 2));
output.write(children.size());
for (int offset : offsets) {
writeUnsigned(output, offset, offsetWidth);
}
output.writeBytes(values.toByteArray());
return output.toByteArray();
}
private static byte[] rawObject(
int idWidth,
int offsetWidth,
int[] ids,
int[] offsets,
byte[] values) {
if (offsets.length != ids.length + 1 || ids.length > 255) {
throw new IllegalArgumentException("Invalid compact raw object shape");
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(2 | ((offsetWidth - 1) << 2) | ((idWidth - 1) << 4));
output.write(ids.length);
for (int id : ids) {
writeUnsigned(output, id, idWidth);
}
for (int offset : offsets) {
writeUnsigned(output, offset, offsetWidth);
}
output.writeBytes(values);
return output.toByteArray();
}
private static byte[] shortString(String value) {
byte[] encoded = utf8(value);
if (encoded.length > 63) {
throw new IllegalArgumentException("Not a short string");
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write((encoded.length << 2) | 1);
output.writeBytes(encoded);
return output.toByteArray();
}
private static void writeUnsigned(ByteArrayOutputStream output, long value, int width) {
for (int index = 0; index < width; ++index) {
output.write((int) (value >>> (index * 8)) & 0xFF);
}
}
private static int minUnsignedWidth(long value) {
if (value <= 0xFFL) {
return 1;
}
if (value <= 0xFFFFL) {
return 2;
}
if (value <= 0xFFFFFFL) {
return 3;
}
return 4;
}
private static List<String> numberedKeys(int count) {
List<String> keys = new ArrayList<>();
for (int index = 0; index < count; ++index) {
keys.add(String.format(Locale.ROOT, "k%03d", index));
}
return keys;
}
private static String nullObjectExpected(List<String> keys) {
StringBuilder result = new StringBuilder("object{");
for (int index = 0; index < keys.size(); ++index) {
if (index != 0) {
result.append(';');
}
result.append(hexUtf8(keys.get(index))).append("=null");
}
return result.append('}').toString();
}
private static String repeatedArrayExpected(int count) {
StringBuilder result = new StringBuilder("array[");
for (int index = 0; index < count; ++index) {
if (index != 0) {
result.append(';');
}
result.append("null");
}
return result.append(']').toString();
}
private static byte[] concat(byte[] left, byte[] right) {
byte[] result = Arrays.copyOf(left, left.length + right.length);
System.arraycopy(right, 0, result, left.length, right.length);
return result;
}
private static byte[] bytes(ByteBuffer buffer) {
ByteBuffer copy = buffer.duplicate();
byte[] result = new byte[copy.remaining()];
copy.get(result);
return result;
}
private static byte[] utf8(String value) {
return value.getBytes(StandardCharsets.UTF_8);
}
private static String hexUtf8(String value) {
return HEX.formatHex(utf8(value));
}
private static void knownIncompatibilityCheck() {
String bmp = "\uE000";
String supplementary = "\uD800\uDC00";
if (Integer.signum(bmp.compareTo(supplementary)) != 1
|| Integer.signum(UNSIGNED_UTF8.compare(bmp, supplementary)) != -1) {
throw new IllegalStateException("Unicode ordering counterexample no longer reproduces");
}
VariantBuilder builder = new VariantBuilder();
VariantObjectBuilder object = builder.startObject();
object.appendKey(bmp);
object.appendInt(1);
object.appendKey(supplementary);
object.appendInt(2);
builder.endObject();
Variant variant = builder.build();
String metadataHex = HEX.formatHex(bytes(variant.getMetadataBuffer()));
String valueHex = HEX.formatHex(bytes(variant.getValueBuffer()));
if (!metadataHex.equals("0102000307ee8080f0908080")
|| !valueHex.equals("0202010000050a14020000001401000000")
|| variant.getFieldAtIndex(0).key.codePointAt(0) != 0x10000
|| variant.getFieldAtIndex(1).key.codePointAt(0) != 0xE000
|| variant.getFieldByKey(bmp).getInt() != 1
|| variant.getFieldByKey(supplementary).getInt() != 2) {
throw new IllegalStateException("parquet-java Unicode deviation evidence changed");
}
}
private static void verifyRealFourByteBoundaries() {
String hugeKey = "k".repeat(0x1000000);
VariantBuilder objectBuilder = new VariantBuilder();
VariantObjectBuilder object = objectBuilder.startObject();
object.appendKey(hugeKey);
object.appendNull();
objectBuilder.endObject();
Variant objectVariant = objectBuilder.build();
int metadataWidth = ((objectVariant.getMetadataBuffer().get(0) >>> 6) & 0x03) + 1;
if (metadataWidth != 4 || objectVariant.getFieldByKey(hugeKey) == null) {
throw new IllegalStateException("Real metadata 3-to-4-byte boundary failed");
}
byte[] payload = new byte[0x1000000 - 5];
Arrays.fill(payload, (byte) 0xAB);
VariantBuilder arrayBuilder = new VariantBuilder();
VariantArrayBuilder array = arrayBuilder.startArray();
array.appendBinary(ByteBuffer.wrap(payload));
arrayBuilder.endArray();
Variant arrayVariant = arrayBuilder.build();
int valueHeader = (arrayVariant.getValueBuffer().get(0) & 0xFF) >>> 2;
int offsetWidth = (valueHeader & 0x03) + 1;
ByteBuffer decoded = arrayVariant.getElementAtIndex(0).getBinary();
if (offsetWidth != 4
|| decoded.remaining() != payload.length
|| !Arrays.equals(payload, bytes(decoded))) {
throw new IllegalStateException("Real value-offset 3-to-4-byte boundary failed");
}
}
}