This is an automated email from the ASF dual-hosted git repository.
szehon-ho pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 6cd6183336 Spark 4.2: Set Iceberg field IDs as Spark column IDs for
DSv2 validation (#17958)
6cd6183336 is described below
commit 6cd6183336c86999648164fd2128c4985c17fb30
Author: Rahul Shivu Mahadev <[email protected]>
AuthorDate: Mon Oct 19 15:57:08 2026 -0700
Spark 4.2: Set Iceberg field IDs as Spark column IDs for DSv2 validation
(#17958)
---
.../TestIncrementallyConstructedQueries.java | 11 ++++----
.../org/apache/iceberg/spark/TypeToSparkType.java | 4 ++-
.../iceberg/spark/TestSparkCatalogOperations.java | 4 ++-
.../apache/iceberg/spark/TestSparkSchemaUtil.java | 30 ++++++++++++++++++++++
.../TestMetadataTablesWithPartitionEvolution.java | 10 ++++++--
5 files changed, 49 insertions(+), 10 deletions(-)
diff --git
a/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestIncrementallyConstructedQueries.java
b/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestIncrementallyConstructedQueries.java
index a5b63c3580..6c4c1985a0 100644
---
a/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestIncrementallyConstructedQueries.java
+++
b/spark/v4.2/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestIncrementallyConstructedQueries.java
@@ -239,12 +239,11 @@ public class TestIncrementallyConstructedQueries extends
ExtensionsTestBase {
// join two DataFrames (QueryExecution #3)
Dataset<Row> joined = df1.join(df2, df1.col("id").equalTo(df2.col("id")));
- // query should resolve columns by name and show null for old data
- List<Object[]> result = rowsToJava(joined.collectAsList());
- assertThat(result)
- .as("Join should resolve columns by name")
- .hasSize(2)
- .containsExactlyInAnyOrder(row(1, null, 1, null), row(2, 200, 2, 200));
+ // the re-added column has a new field ID, so Spark detects that df1 is
out of date
+ assertThatThrownBy(joined::collect)
+ .isInstanceOf(AnalysisException.class)
+
.hasMessageContaining("INCOMPATIBLE_TABLE_CHANGE_AFTER_ANALYSIS.COLUMNS_MISMATCH")
+ .hasMessageContaining("`salary` field ID has changed from 2 to 3");
}
@TestTemplate
diff --git
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/TypeToSparkType.java
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/TypeToSparkType.java
index dc07793757..7290099d5b 100644
---
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/TypeToSparkType.java
+++
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/TypeToSparkType.java
@@ -74,7 +74,9 @@ class TypeToSparkType extends
TypeUtil.SchemaVisitor<DataType> {
Types.NestedField field = fields.get(i);
DataType type = fieldResults.get(i);
Metadata metadata = fieldMetadata(field.fieldId());
- StructField sparkField = StructField.apply(field.name(), type,
field.isOptional(), metadata);
+ StructField sparkField =
+ StructField.apply(field.name(), type, field.isOptional(), metadata)
+ .withId(Integer.toString(field.fieldId()));
if (field.doc() != null) {
sparkField = sparkField.withComment(field.doc());
}
diff --git
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkCatalogOperations.java
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkCatalogOperations.java
index 27d99127b7..b1f2137073 100644
---
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkCatalogOperations.java
+++
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkCatalogOperations.java
@@ -134,7 +134,9 @@ public class TestSparkCatalogOperations extends
CatalogTestBase {
assertThat(table).as("Should return updated table").isNotNull();
- Column expectedField = Column.create(fieldName, DataTypes.StringType,
true);
+ // the new column carries the field ID that Iceberg assigned to it
+ Column expectedField =
+ Column.builderFor(fieldName,
DataTypes.StringType).nullable(true).id("3").build();
assertThat(table.columns())
.as("Adding a column to a table should return the updated table with
the new column")
.contains(expectedField, atIndex(2));
diff --git
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
index ba0dfd8411..d1c01d4e4d 100644
---
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
+++
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
@@ -37,6 +37,8 @@ import
org.apache.spark.sql.catalyst.expressions.AttributeReference;
import org.apache.spark.sql.catalyst.expressions.MetadataAttribute;
import org.apache.spark.sql.catalyst.types.DataTypeUtils;
import org.apache.spark.sql.catalyst.util.ResolveDefaultColumnsUtils$;
+import org.apache.spark.sql.connector.catalog.CatalogV2Util;
+import org.apache.spark.sql.connector.catalog.Column;
import org.apache.spark.sql.types.DataType;
import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.types.GeographyType;
@@ -101,6 +103,34 @@ public class TestSparkSchemaUtil {
}
}
+ @Test
+ public void testFieldIDsInSparkColumns() {
+ Schema schema =
+ new Schema(
+ optional(
+ 10,
+ "location",
+ Types.StructType.of(
+ optional(11, "latitude", Types.DoubleType.get()),
+ optional(12, "longitude", Types.DoubleType.get()))));
+
+ StructType sparkSchema = SparkSchemaUtil.convert(schema);
+ StructField location = sparkSchema.apply("location");
+ StructType locationType = (StructType) location.dataType();
+
+ assertThat(location.id().get()).isEqualTo("10");
+ assertThat(locationType.apply("latitude").id().get()).isEqualTo("11");
+ assertThat(locationType.apply("longitude").id().get()).isEqualTo("12");
+
+ Column[] columns = CatalogV2Util.structTypeToV2Columns(sparkSchema, true);
+ assertThat(columns).hasSize(1);
+ assertThat(columns[0].id()).isEqualTo("10");
+
+ StructType columnType = (StructType) columns[0].dataType();
+ assertThat(columnType.apply("latitude").id().get()).isEqualTo("11");
+ assertThat(columnType.apply("longitude").id().get()).isEqualTo("12");
+ }
+
@Test
public void testGeospatialTypeConversion() {
// a default-CRS geometry round-trips through the null <-> OGC:CRS84
normalization
diff --git
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestMetadataTablesWithPartitionEvolution.java
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestMetadataTablesWithPartitionEvolution.java
index a417454b45..c55b569107 100644
---
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestMetadataTablesWithPartitionEvolution.java
+++
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestMetadataTablesWithPartitionEvolution.java
@@ -58,6 +58,7 @@ import org.apache.spark.sql.Row;
import org.apache.spark.sql.catalyst.parser.ParseException;
import org.apache.spark.sql.types.DataType;
import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.util.SchemaUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -670,20 +671,25 @@ public class TestMetadataTablesWithPartitionEvolution
extends CatalogTestBase {
df = df.filter(filter);
}
+ // the expected type is parsed from DDL and has no field IDs, so compare
without them
DataType expectedType =
spark.sessionState().sqlParser().parseDataType(expectedTypeAsString);
switch (tableType) {
case PARTITIONS:
case FILES:
case ALL_DATA_FILES:
DataType actualFilesType = df.schema().apply("partition").dataType();
- assertThat(actualFilesType).as("Partition type must
match").isEqualTo(expectedType);
+ assertThat(SchemaUtils.clearFieldIds(actualFilesType))
+ .as("Partition type must match")
+ .isEqualTo(expectedType);
break;
case ENTRIES:
case ALL_ENTRIES:
StructType dataFileType = (StructType)
df.schema().apply("data_file").dataType();
DataType actualEntriesType =
dataFileType.apply("partition").dataType();
- assertThat(actualEntriesType).as("Partition type must
match").isEqualTo(expectedType);
+ assertThat(SchemaUtils.clearFieldIds(actualEntriesType))
+ .as("Partition type must match")
+ .isEqualTo(expectedType);
break;
default: