Skip to content

Commit 23fe7bf

Browse files
authored
feat(variant): Add support to write shredded variants for HoodieRecordType.AVRO (apache#18065)
Adds write support for shredded Variant on the AVRO record path. Variants are decomposed into a typed_value column for better compression and column pruning. Shredding is delegated to a VariantShreddingProvider (in hudi-common, loaded reflectively), with a Spark 4 implementation built on Spark's VariantShreddingWriter. - Config: new advanced hoodie.parquet.variant.shredding.provider.class (sinceVersion 1.3.0), auto-detected from the classpath when unset. Gated by the existing write.shredding.enabled; reads governed by allow.reading.shredded. - Write path: HoodieAvroFileWriterFactory builds the Parquet schema from the effective (shredded) schema so value is nullable and typed_value is present; injects the provider on a copied Properties so the shared config is never mutated. Fails fast when shredding is required but no provider is available, and on the not-yet-supported read-then-reshred path (apache#18931). - Spark read: BaseSpark4Adapter recognizes both unshredded (2-field) and shredded (3-field) physical layouts as Variant. Spark 4.0.x keeps a reorder workaround for its hardcoded [value, metadata] converter; 4.1+ reads by name (SPARK-54410), removable later (apache#18935). No session SQLConf mutation - Spark's converter reads the flag via SQLConf.get. - DDL parsing: shared StringUtils.splitTopLevelCommas (paren-aware) keeps decimal(15, 1) intact, reused by both the shredding DDL parser and the vector column metadata parser. - Tests: Spark integration (TestVariantDataType) plus unit coverage for the forced-shredding DDL (incl. decimal), shredded schema shape, the read-then-reshred guard, and splitTopLevelCommas. Limitations: shredded writes require Spark 4.0+; reading shredded data back requires Spark 4.1+ (Spark 4.0 read deferred, apache#18931). Closes apache#18066 Closes apache#17748
1 parent 5de762f commit 23fe7bf

13 files changed

Lines changed: 1290 additions & 46 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.hudi.avro;
21+
22+
import org.apache.hudi.common.schema.HoodieSchema;
23+
24+
import org.apache.avro.Schema;
25+
import org.apache.avro.generic.GenericRecord;
26+
27+
/**
28+
* Interface for shredding variant values at write time.
29+
* <p>
30+
* Implementations parse variant binary data (value + metadata bytes) and produce
31+
* a shredded {@link GenericRecord} with typed_value columns populated according
32+
* to the shredding schema.
33+
* <p>
34+
* This interface allows the variant binary parsing logic (which may depend on
35+
* engine-specific libraries like Spark's variant module) to be loaded via reflection,
36+
* keeping the core write support free of engine-specific dependencies.
37+
*/
38+
public interface VariantShreddingProvider {
39+
40+
/**
41+
* Transform an unshredded variant GenericRecord into a shredded one.
42+
* <p>
43+
* The input record is expected to have:
44+
* <ul>
45+
* <li>{@code value}: ByteBuffer containing the variant value binary</li>
46+
* <li>{@code metadata}: ByteBuffer containing the variant metadata binary</li>
47+
* </ul>
48+
* <p>
49+
* The output record should conform to {@code shreddedSchema} and have:
50+
* <ul>
51+
* <li>{@code value}: ByteBuffer or null (null when typed_value captures the full value)</li>
52+
* <li>{@code metadata}: ByteBuffer (always present)</li>
53+
* <li>{@code typed_value}: the typed representation extracted from the variant binary,
54+
* or null if the variant type does not match the typed_value schema</li>
55+
* </ul>
56+
*
57+
* @param unshreddedVariant GenericRecord with {value: ByteBuffer, metadata: ByteBuffer}
58+
* @param shreddedSchema target Avro schema with {value: nullable ByteBuffer, metadata: ByteBuffer, typed_value: type}
59+
* @param variantSchema HoodieSchema.Variant containing the shredding schema information
60+
* @return a GenericRecord conforming to shreddedSchema with typed_value populated where possible
61+
*/
62+
GenericRecord shredVariantRecord(
63+
GenericRecord unshreddedVariant,
64+
Schema shreddedSchema,
65+
HoodieSchema.Variant variantSchema);
66+
}

hudi-common/src/main/java/org/apache/hudi/common/config/HoodieStorageConfig.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,12 @@ public class HoodieStorageConfig extends HoodieConfig {
235235
.noDefaultValue()
236236
.markAdvanced()
237237
.sinceVersion("1.1.0")
238-
.withDocumentation("Forces a specific shredding schema for all variant columns, intended for testing. "
238+
.withDocumentation("Test-only: forces a specific shredding schema for all variant columns. Not intended "
239+
+ "for production use; it exists solely to exercise the shredding write path in tests, mirroring "
240+
+ "Spark's internal spark.sql.variant.forceShreddingSchemaForTest. "
239241
+ "The value should be a DDL-format schema string (e.g., 'a int, b string, c decimal(15, 1)'). "
240242
+ "When set and write shredding is enabled, this schema overrides the schema-driven shredding "
241-
+ "configuration for all variant columns. "
242-
+ "Equivalent to Spark's spark.sql.variant.forceShreddingSchemaForTest.");
243+
+ "configuration for all variant columns.");
243244

244245
public static final ConfigProperty<Boolean> PARQUET_VARIANT_ALLOW_READING_SHREDDED = ConfigProperty
245246
.key("hoodie.parquet.variant.allow.reading.shredded")
@@ -250,6 +251,16 @@ public class HoodieStorageConfig extends HoodieConfig {
250251
+ "When disabled, only unshredded variant data can be read. "
251252
+ "Equivalent to Spark's spark.sql.variant.allowReadingShredded.");
252253

254+
public static final ConfigProperty<String> PARQUET_VARIANT_SHREDDING_PROVIDER_CLASS = ConfigProperty
255+
.key("hoodie.parquet.variant.shredding.provider.class")
256+
.noDefaultValue()
257+
.markAdvanced()
258+
.sinceVersion("1.3.0")
259+
.withDocumentation("Fully-qualified class name of the VariantShreddingProvider implementation "
260+
+ "used to shred variant values at write time in the Avro record path. "
261+
+ "The provider parses variant binary data and populates typed_value columns. "
262+
+ "When not set, the provider is auto-detected from the classpath.");
263+
253264
public static final ConfigProperty<Boolean> WRITE_UTC_TIMEZONE = ConfigProperty
254265
.key("hoodie.parquet.write.utc-timezone.enabled")
255266
.defaultValue(true)
@@ -596,11 +607,6 @@ public Builder parquetVariantWriteShreddingEnabled(boolean enabled) {
596607
return this;
597608
}
598609

599-
public Builder parquetVariantForceShreddingSchemaForTest(String schemaString) {
600-
storageConfig.setValue(PARQUET_VARIANT_FORCE_SHREDDING_SCHEMA_FOR_TEST, schemaString);
601-
return this;
602-
}
603-
604610
public Builder parquetVariantAllowReadingShredded(boolean allowed) {
605611
storageConfig.setValue(PARQUET_VARIANT_ALLOW_READING_SHREDDED, String.valueOf(allowed));
606612
return this;

hudi-common/src/main/java/org/apache/hudi/common/schema/HoodieSchema.java

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.apache.hudi.avro.AvroSchemaUtils;
2222
import org.apache.hudi.common.util.Option;
23+
import org.apache.hudi.common.util.StringUtils;
2324
import org.apache.hudi.common.util.ValidationUtils;
2425
import org.apache.hudi.common.util.collection.Pair;
2526
import org.apache.hudi.exception.HoodieAvroSchemaException;
@@ -282,35 +283,18 @@ public static String buildVectorColumnsMetadataValue(HoodieSchema schema) {
282283
* @return set of field names (preserves insertion order), or empty set if input is null / empty
283284
*/
284285
public static Set<String> parseVectorColumnNames(String footerValue) {
285-
if (footerValue == null || footerValue.isEmpty()) {
286-
return Collections.emptySet();
287-
}
288286
LinkedHashSet<String> names = new LinkedHashSet<>();
289-
int depth = 0;
290-
int start = 0;
291-
for (int i = 0; i < footerValue.length(); i++) {
292-
char c = footerValue.charAt(i);
293-
if (c == '(') {
294-
depth++;
295-
} else if (c == ')') {
296-
depth--;
297-
} else if (c == ',' && depth == 0) {
298-
addVectorColumnName(footerValue, start, i, names);
299-
start = i + 1;
287+
// Split on top-level commas only so the comma inside a VECTOR(dim, elemType) descriptor is
288+
// not mistaken for a column separator.
289+
for (String entry : StringUtils.splitTopLevelCommas(footerValue)) {
290+
int colon = entry.indexOf(':');
291+
if (colon > 0) {
292+
names.add(entry.substring(0, colon).trim());
300293
}
301294
}
302-
addVectorColumnName(footerValue, start, footerValue.length(), names);
303295
return names;
304296
}
305297

306-
private static void addVectorColumnName(String s, int start, int end, Set<String> names) {
307-
int colon = s.indexOf(':', start);
308-
if (colon > start && colon < end) {
309-
names.add(s.substring(start, colon).trim());
310-
}
311-
}
312-
313-
314298
private Schema avroSchema;
315299
private HoodieSchemaType type;
316300
private transient List<HoodieSchemaField> fields;

0 commit comments

Comments
 (0)