Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
10ec210
Upgrade hudi version in xtable
vinishjail97 Dec 17, 2025
bc6b611
Fix hudi source tests
vinishjail97 Dec 17, 2025
9246036
Fix few more tests
vinishjail97 Dec 17, 2025
f854079
Fix more tests
vinishjail97 Dec 18, 2025
c8b23d5
Fix more tests-2
vinishjail97 Dec 18, 2025
61e48de
Remove zero row group test
vinishjail97 Dec 19, 2025
6eba339
Disable test for Paimon source, Hudi target and un-parittioned
vinishjail97 Dec 19, 2025
43ff8bb
Fix more tests-4
vinishjail97 Dec 19, 2025
6923779
Fix more tests-5
vinishjail97 Dec 20, 2025
8d3c7e0
Address self review comments and link GH issues for failing tests
vinishjail97 Dec 23, 2025
fec7f90
Merge branch 'main' into hudi-upgrade-version
vinishjail97 Jun 1, 2026
4058fb8
Upgrade hudi to 1.2.0 and align spark/delta with main
vinishjail97 Jun 1, 2026
19606d3
Fix hudi unit-test regressions from the 1.2.0 upgrade
vinishjail97 Jun 1, 2026
de2164b
Fix HoodieAvroWriteSupportWithFieldIds ctor for hudi 1.2.0
vinishjail97 Jun 1, 2026
ccf37ff
Hudi 1.2.0: gate col-stats to un-partitioned tables and fix partition…
vinishjail97 Jun 24, 2026
2f8ced3
Exclude jetty-runner so Hudi 1.2 TimelineService gets jetty-util 9.4.x
vinishjail97 Jun 24, 2026
42bba3a
Address review feedback: comment formatting, scala-version pom, remov…
vinishjail97 Jun 29, 2026
ac47e46
Restore default_hudi database name for Hudi target tables
vinishjail97 Jun 29, 2026
7a74485
Derive Hudi target database name from the target namespace
vinishjail97 Jun 29, 2026
b426c1f
Revert IdTracker to single-path generateIdMappings, keep only the API…
vinishjail97 Jun 29, 2026
ad48872
Add semantic WriteStatus comparison in TestBaseFileUpdatesExtractor
vinishjail97 Jun 29, 2026
c7cd096
Tidy up comments in BaseFileUpdatesExtractor test
vinishjail97 Jun 29, 2026
e75c4ea
Enable col-stats tests at table version 6 and link #834
vinishjail97 Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
<lombok.version>1.18.36</lombok.version>
<lombok-maven-plugin.version>1.18.20.0</lombok-maven-plugin.version>
<hadoop.version>3.4.1</hadoop.version>
<hudi.version>0.14.0</hudi.version>
<hudi.version>1.2.0</hudi.version>
<aws.version>2.29.40</aws.version>
<hive.version>2.3.9</hive.version>
<hive.version>3.1.3</hive.version>
<maven-source-plugin.version>3.3.1</maven-source-plugin.version>
<maven-javadoc-plugin.version>3.8.0</maven-javadoc-plugin.version>
<maven-gpg-plugin.version>3.2.4</maven-gpg-plugin.version>
Expand Down Expand Up @@ -271,6 +271,12 @@
<version>${hudi.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hudi</groupId>
<artifactId>hudi-utilities_${scala.binary.version}</artifactId>
<version>${hudi.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hudi</groupId>
<artifactId>hudi-spark${spark.version.prefix}-bundle_${scala.binary.version}</artifactId>
Expand Down Expand Up @@ -738,6 +744,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${maven.compiler.target}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* 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.
*/

package org.apache.hudi.stats;

import static org.apache.xtable.model.schema.InternalSchema.MetadataKey.TIMESTAMP_PRECISION;
import static org.apache.xtable.model.schema.InternalSchema.MetadataValue.MICROS;

import java.lang.reflect.Constructor;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;

import org.apache.hudi.metadata.HoodieIndexVersion;

import org.apache.xtable.model.schema.InternalSchema;
import org.apache.xtable.model.stat.ColumnStat;

/**
* Utility class for creating and converting Hudi {@link ValueMetadata} instances from XTable's
* internal schema representation.
*
* <p>This class bridges XTable's {@link InternalSchema} types to Hudi's {@link ValueType} and
* {@link ValueMetadata} used for column statistics. It handles the conversion of various data types
* including timestamps, decimals, and dates.
*
* <p>Note: This class uses reflection to create {@link ValueMetadata} instances because XTable
* classes may be loaded by a different classloader than Hudi classes in Spark environments, making
* direct constructor access illegal.
*/
public class XTableValueMetadata {
Comment thread
vinishjail97 marked this conversation as resolved.
Comment thread
vinishjail97 marked this conversation as resolved.

/**
* Creates a {@link ValueMetadata} instance from a {@link ColumnStat} for the specified Hudi index
* version.
*
* @param columnStat the column statistics containing schema information
* @param indexVersion the Hudi index version to use for metadata creation
* @return the appropriate {@link ValueMetadata} for the column's data type
* @throws IllegalArgumentException if columnStat is null (for V2+ index), or if decimal metadata
* is missing required precision/scale
* @throws IllegalStateException if an unsupported internal type is encountered
*/
public static ValueMetadata getValueMetadata(
ColumnStat columnStat, HoodieIndexVersion indexVersion) {
if (indexVersion.lowerThan(HoodieIndexVersion.V2)) {
return ValueMetadata.V1EmptyMetadata.get();
}
if (columnStat == null) {
throw new IllegalArgumentException("ColumnStat cannot be null");
}
InternalSchema internalSchema = columnStat.getField().getSchema();
ValueType valueType = fromInternalSchema(internalSchema);
if (valueType == ValueType.V1) {
throw new IllegalStateException(
"InternalType V1 should not be returned from fromInternalSchema");
} else if (valueType == ValueType.DECIMAL) {
if (internalSchema.getMetadata() == null) {
throw new IllegalArgumentException("Decimal metadata is null");
} else if (!internalSchema
.getMetadata()
.containsKey(InternalSchema.MetadataKey.DECIMAL_SCALE)) {
throw new IllegalArgumentException("Decimal scale is null");
} else if (!internalSchema
.getMetadata()
.containsKey(InternalSchema.MetadataKey.DECIMAL_PRECISION)) {
throw new IllegalArgumentException("Decimal precision is null");
}
int scale = (int) internalSchema.getMetadata().get(InternalSchema.MetadataKey.DECIMAL_SCALE);
int precision =
(int) internalSchema.getMetadata().get(InternalSchema.MetadataKey.DECIMAL_PRECISION);
return ValueMetadata.DecimalMetadata.create(precision, scale);
} else {
return createValueMetadata(valueType);
}
}

/**
* Maps an XTable {@link InternalSchema} to the corresponding Hudi {@link ValueType}.
*
* @param internalSchema the internal schema to convert
* @return the corresponding Hudi value type
* @throws UnsupportedOperationException if the internal data type is not supported
*/
static ValueType fromInternalSchema(InternalSchema internalSchema) {
switch (internalSchema.getDataType()) {
case NULL:
return ValueType.NULL;
case BOOLEAN:
return ValueType.BOOLEAN;
case INT:
return ValueType.INT;
case LONG:
return ValueType.LONG;
case FLOAT:
return ValueType.FLOAT;
case DOUBLE:
return ValueType.DOUBLE;
case STRING:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add ENUM here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added — ENUM is handled in fromInternalSchema (it falls through with STRING, since enum values are stored as their string symbols in column stats), and there's a unit test for it (testFromInternalSchemaEnumMapsToString in TestXTableValueMetadata).

case ENUM:
// Enum values are stored as their string symbols in column statistics.
return ValueType.STRING;
case BYTES:
return ValueType.BYTES;
case FIXED:
return ValueType.FIXED;
case DECIMAL:
return ValueType.DECIMAL;
case UUID:
return ValueType.UUID;
case DATE:
return ValueType.DATE;
case TIMESTAMP:
if (internalSchema.getMetadata() != null
&& MICROS == internalSchema.getMetadata().get(TIMESTAMP_PRECISION)) {
return ValueType.TIMESTAMP_MICROS;
} else {
return ValueType.TIMESTAMP_MILLIS;
}
case TIMESTAMP_NTZ:
if (internalSchema.getMetadata() != null
&& MICROS == internalSchema.getMetadata().get(TIMESTAMP_PRECISION)) {
return ValueType.LOCAL_TIMESTAMP_MICROS;
} else {
return ValueType.LOCAL_TIMESTAMP_MILLIS;
}
default:
throw new UnsupportedOperationException(
"InternalType " + internalSchema.getDataType() + " is not supported");
}
}

/**
* Creates a {@link ValueMetadata} instance from a {@link ValueType} for the specified Hudi index
* version. This method is primarily intended for testing purposes.
*
* @param valueType the Hudi value type
* @param indexVersion the Hudi index version to use for metadata creation
* @return the appropriate {@link ValueMetadata} for the value type
*/
public static ValueMetadata getValueMetadata(
ValueType valueType, HoodieIndexVersion indexVersion) {
if (indexVersion.lowerThan(HoodieIndexVersion.V2)) {
return ValueMetadata.V1EmptyMetadata.get();
}
return createValueMetadata(valueType);
}

/**
* Creates a ValueMetadata instance using reflection to access the protected constructor. This is
* necessary because XTable classes may be loaded by a different classloader than Hudi classes in
* Spark environments, making direct constructor access illegal.
*/
private static ValueMetadata createValueMetadata(ValueType valueType) {
try {
Constructor<ValueMetadata> constructor =
ValueMetadata.class.getDeclaredConstructor(ValueType.class);
constructor.setAccessible(true);
return constructor.newInstance(valueType);
} catch (Exception e) {
throw new RuntimeException(
"Failed to create ValueMetadata instance for type: " + valueType, e);
}
}

/**
* Converts a value from its XTable representation to the appropriate Hudi range type for column
* statistics.
*
* <p>This method handles the conversion of temporal types ({@link Instant}, {@link
* LocalDateTime}, {@link LocalDate}) to their corresponding Hudi representations based on the
* value metadata.
*
* @param val the value to convert
* @param valueMetadata the metadata describing the target value type
* @return the converted value suitable for Hudi range statistics
* @throws IllegalArgumentException if the value type doesn't match the expected metadata type
*/
public static Comparable<?> convertHoodieTypeToRangeType(
Comparable<?> val, ValueMetadata valueMetadata) {
if (val instanceof Instant) {
if (valueMetadata.getValueType().equals(ValueType.TIMESTAMP_MILLIS)) {
return ValueType.fromTimestampMillis(val, valueMetadata);
} else if (valueMetadata.getValueType().equals(ValueType.TIMESTAMP_MICROS)) {
return ValueType.fromTimestampMicros(val, valueMetadata);
} else {
throw new IllegalArgumentException(
"Unsupported value type: " + valueMetadata.getValueType());
}
} else if (val instanceof LocalDateTime) {
if (valueMetadata.getValueType().equals(ValueType.LOCAL_TIMESTAMP_MILLIS)) {
return ValueType.fromLocalTimestampMillis(val, valueMetadata);
} else if (valueMetadata.getValueType().equals(ValueType.LOCAL_TIMESTAMP_MICROS)) {
return ValueType.fromLocalTimestampMicros(val, valueMetadata);
} else {
throw new IllegalArgumentException(
"Unsupported value type: " + valueMetadata.getValueType());
}
} else if (val instanceof LocalDate) {
if (valueMetadata.getValueType().equals(ValueType.DATE)) {
return ValueType.fromDate(val, valueMetadata);
} else {
throw new IllegalArgumentException(
"Unsupported value type: " + valueMetadata.getValueType());
}
} else {
return val;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,9 @@ private Schema fromInternalSchema(InternalSchema internalSchema, String currentP
return finalizeSchema(
LogicalTypes.date().addToSchema(Schema.create(Schema.Type.INT)), internalSchema);
case TIMESTAMP:
if (internalSchema.getMetadata().get(InternalSchema.MetadataKey.TIMESTAMP_PRECISION)
== InternalSchema.MetadataValue.MICROS) {
if (internalSchema.getMetadata() != null
&& internalSchema.getMetadata().get(InternalSchema.MetadataKey.TIMESTAMP_PRECISION)
== InternalSchema.MetadataValue.MICROS) {
return finalizeSchema(
LogicalTypes.timestampMicros().addToSchema(Schema.create(Schema.Type.LONG)),
internalSchema);
Expand All @@ -402,8 +403,9 @@ private Schema fromInternalSchema(InternalSchema internalSchema, String currentP
internalSchema);
}
case TIMESTAMP_NTZ:
if (internalSchema.getMetadata().get(InternalSchema.MetadataKey.TIMESTAMP_PRECISION)
== InternalSchema.MetadataValue.MICROS) {
if (internalSchema.getMetadata() != null
&& internalSchema.getMetadata().get(InternalSchema.MetadataKey.TIMESTAMP_PRECISION)
== InternalSchema.MetadataValue.MICROS) {
return finalizeSchema(
LogicalTypes.localTimestampMicros().addToSchema(Schema.create(Schema.Type.LONG)),
internalSchema);
Expand Down
Loading