This is an automated email from the ASF dual-hosted git repository.

danielcweeks 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 8f825e6e19 Spark: support default values with DDL (#18250)
8f825e6e19 is described below

commit 8f825e6e19088f5b0db2e5095f49bd18ffafeb92
Author: Daniel Weeks <[email protected]>
AuthorDate: Thu Oct 1 08:17:56 2026 -0700

    Spark: support default values with DDL (#18250)
    
    * Spark: support default values with DDL
---
 .../action/IcebergDataCompactionBenchmark.java     |   9 +-
 .../action/IcebergSortCompactionBenchmark.java     |   8 +-
 .../java/org/apache/iceberg/spark/BaseCatalog.java |   7 +
 .../org/apache/iceberg/spark/SparkCatalog.java     |  49 +++---
 .../org/apache/iceberg/spark/SparkSchemaUtil.java  |  35 ++++
 .../apache/iceberg/spark/SparkSessionCatalog.java  |  43 +++--
 .../org/apache/iceberg/spark/SparkTypeToType.java  |  80 ++++++++-
 .../org/apache/iceberg/spark/SparkV2Filters.java   |   8 +-
 .../actions/BaseTableCreationSparkAction.java      |  10 +-
 .../iceberg/spark/TestSparkSessionCatalog.java     |  24 ++-
 .../actions/TestRemoveOrphanFilesAction3.java      |  41 ++++-
 .../iceberg/spark/source/TestPathIdentifier.java   |  14 +-
 .../iceberg/spark/sql/TestSparkDefaultValues.java  | 178 +++++++++++++++++++--
 13 files changed, 406 insertions(+), 100 deletions(-)

diff --git 
a/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergDataCompactionBenchmark.java
 
b/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergDataCompactionBenchmark.java
index 6199726e18..de00afd376 100644
--- 
a/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergDataCompactionBenchmark.java
+++ 
b/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergDataCompactionBenchmark.java
@@ -24,7 +24,6 @@ import static org.apache.spark.sql.functions.col;
 import static org.apache.spark.sql.functions.concat;
 import static org.apache.spark.sql.functions.lit;
 
-import java.util.Collections;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.actions.SizeBasedFileRewritePlanner;
 import org.apache.iceberg.spark.Spark3Util;
@@ -35,7 +34,7 @@ import org.apache.iceberg.types.Types;
 import org.apache.spark.sql.Dataset;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.connector.catalog.Identifier;
-import org.apache.spark.sql.connector.expressions.Transform;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.Measurement;
 import org.openjdk.jmh.annotations.Param;
@@ -92,8 +91,10 @@ public class IcebergDataCompactionBenchmark extends 
IcebergCompactionBenchmark {
           (SparkSessionCatalog<?>)
               Spark3Util.catalogAndIdentifier(spark(), 
"spark_catalog").catalog();
       catalog.dropTable(IDENT);
-      catalog.createTable(
-          IDENT, SparkSchemaUtil.convert(schema), new Transform[0], 
Collections.emptyMap());
+
+      TableInfo tableInfo =
+          new 
TableInfo.Builder().withSchema(SparkSchemaUtil.convert(schema)).build();
+      catalog.createTable(IDENT, tableInfo);
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
diff --git 
a/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergSortCompactionBenchmark.java
 
b/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergSortCompactionBenchmark.java
index 8900ae8136..d4d21dde93 100644
--- 
a/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergSortCompactionBenchmark.java
+++ 
b/spark/v4.2/spark/src/jmh/java/org/apache/iceberg/spark/action/IcebergSortCompactionBenchmark.java
@@ -25,7 +25,6 @@ import static org.apache.spark.sql.functions.current_date;
 import static org.apache.spark.sql.functions.date_add;
 import static org.apache.spark.sql.functions.expr;
 
-import java.util.Collections;
 import java.util.concurrent.TimeUnit;
 import org.apache.iceberg.NullOrder;
 import org.apache.iceberg.Schema;
@@ -40,7 +39,7 @@ import org.apache.iceberg.types.Types;
 import org.apache.spark.sql.Dataset;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.connector.catalog.Identifier;
-import org.apache.spark.sql.connector.expressions.Transform;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.types.DataTypes;
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.BenchmarkMode;
@@ -268,8 +267,9 @@ public class IcebergSortCompactionBenchmark extends 
IcebergCompactionBenchmark {
           (SparkSessionCatalog<?>)
               Spark3Util.catalogAndIdentifier(spark(), 
"spark_catalog").catalog();
       catalog.dropTable(IDENT);
-      catalog.createTable(
-          IDENT, SparkSchemaUtil.convert(schema), new Transform[0], 
Collections.emptyMap());
+      TableInfo tableInfo =
+          new 
TableInfo.Builder().withSchema(SparkSchemaUtil.convert(schema)).build();
+      catalog.createTable(IDENT, tableInfo);
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/BaseCatalog.java 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/BaseCatalog.java
index 7435fa9874..c2a43306ce 100644
--- a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/BaseCatalog.java
+++ b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/BaseCatalog.java
@@ -18,6 +18,7 @@
  */
 package org.apache.iceberg.spark;
 
+import java.util.Set;
 import org.apache.iceberg.spark.procedures.SparkProcedures;
 import org.apache.iceberg.spark.procedures.SparkProcedures.ProcedureBuilder;
 import org.apache.iceberg.spark.source.HasIcebergCatalog;
@@ -30,6 +31,7 @@ import org.apache.spark.sql.connector.catalog.Relation;
 import org.apache.spark.sql.connector.catalog.RelationCatalog;
 import org.apache.spark.sql.connector.catalog.StagingTableCatalog;
 import org.apache.spark.sql.connector.catalog.SupportsNamespaces;
+import org.apache.spark.sql.connector.catalog.TableCatalogCapability;
 import org.apache.spark.sql.connector.catalog.procedures.UnboundProcedure;
 import org.apache.spark.sql.util.CaseInsensitiveStringMap;
 
@@ -118,4 +120,9 @@ abstract class BaseCatalog
   private static boolean isSystemNamespace(String[] namespace) {
     return namespace.length == 1 && namespace[0].equalsIgnoreCase("system");
   }
+
+  @Override
+  public Set<TableCatalogCapability> capabilities() {
+    return Set.of(TableCatalogCapability.SUPPORT_COLUMN_DEFAULT_VALUE);
+  }
 }
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkCatalog.java 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkCatalog.java
index 67c75c3a63..9e2e7138e0 100644
--- a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkCatalog.java
+++ b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkCatalog.java
@@ -82,10 +82,9 @@ import org.apache.spark.sql.connector.catalog.TableChange;
 import org.apache.spark.sql.connector.catalog.TableChange.ColumnChange;
 import org.apache.spark.sql.connector.catalog.TableChange.RemoveProperty;
 import org.apache.spark.sql.connector.catalog.TableChange.SetProperty;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.connector.catalog.TableSummary;
 import org.apache.spark.sql.connector.catalog.View;
-import org.apache.spark.sql.connector.expressions.Transform;
-import org.apache.spark.sql.types.StructType;
 import org.apache.spark.sql.util.CaseInsensitiveStringMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -202,17 +201,16 @@ public class SparkCatalog extends BaseCatalog {
   }
 
   @Override
-  public Table createTable(
-      Identifier ident, StructType schema, Transform[] transforms, Map<String, 
String> properties)
-      throws TableAlreadyExistsException {
-    Schema icebergSchema = SparkSchemaUtil.convert(schema);
+  public Table createTable(Identifier ident, TableInfo tableInfo)
+      throws TableAlreadyExistsException, NoSuchNamespaceException {
+    Schema icebergSchema = SparkSchemaUtil.convertWithDefaults(tableInfo);
     try {
       Catalog.TableBuilder builder = newBuilder(ident, icebergSchema);
       org.apache.iceberg.Table icebergTable =
           builder
-              .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
transforms))
-              .withLocation(properties.get("location"))
-              .withProperties(Spark3Util.rebuildCreateProperties(properties))
+              .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
tableInfo.partitions()))
+              .withLocation(tableInfo.properties().get("location"))
+              
.withProperties(Spark3Util.rebuildCreateProperties(tableInfo.properties()))
               .create();
       return new SparkTable(icebergTable);
     } catch (AlreadyExistsException e) {
@@ -221,17 +219,16 @@ public class SparkCatalog extends BaseCatalog {
   }
 
   @Override
-  public StagedTable stageCreate(
-      Identifier ident, StructType schema, Transform[] transforms, Map<String, 
String> properties)
+  public StagedTable stageCreate(Identifier ident, TableInfo tableInfo)
       throws TableAlreadyExistsException {
-    Schema icebergSchema = SparkSchemaUtil.convert(schema);
+    Schema icebergSchema = SparkSchemaUtil.convert(tableInfo);
     try {
       Catalog.TableBuilder builder = newBuilder(ident, icebergSchema);
       Transaction transaction =
           builder
-              .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
transforms))
-              .withLocation(properties.get("location"))
-              .withProperties(Spark3Util.rebuildCreateProperties(properties))
+              .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
tableInfo.partitions()))
+              .withLocation(tableInfo.properties().get("location"))
+              
.withProperties(Spark3Util.rebuildCreateProperties(tableInfo.properties()))
               .createTransaction();
       return new StagedSparkTable(transaction);
     } catch (AlreadyExistsException e) {
@@ -240,17 +237,16 @@ public class SparkCatalog extends BaseCatalog {
   }
 
   @Override
-  public StagedTable stageReplace(
-      Identifier ident, StructType schema, Transform[] transforms, Map<String, 
String> properties)
+  public StagedTable stageReplace(Identifier ident, TableInfo tableInfo)
       throws NoSuchTableException {
-    Schema icebergSchema = SparkSchemaUtil.convert(schema);
+    Schema icebergSchema = SparkSchemaUtil.convert(tableInfo);
     try {
       Catalog.TableBuilder builder = newBuilder(ident, icebergSchema);
       Transaction transaction =
           builder
-              .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
transforms))
-              .withLocation(properties.get("location"))
-              .withProperties(Spark3Util.rebuildCreateProperties(properties))
+              .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
tableInfo.partitions()))
+              .withLocation(tableInfo.properties().get("location"))
+              
.withProperties(Spark3Util.rebuildCreateProperties(tableInfo.properties()))
               .replaceTransaction();
       return new StagedSparkTable(transaction);
     } catch (org.apache.iceberg.exceptions.NoSuchTableException e) {
@@ -259,15 +255,14 @@ public class SparkCatalog extends BaseCatalog {
   }
 
   @Override
-  public StagedTable stageCreateOrReplace(
-      Identifier ident, StructType schema, Transform[] transforms, Map<String, 
String> properties) {
-    Schema icebergSchema = SparkSchemaUtil.convert(schema);
+  public StagedTable stageCreateOrReplace(Identifier ident, TableInfo 
tableInfo) {
+    Schema icebergSchema = SparkSchemaUtil.convert(tableInfo);
     Catalog.TableBuilder builder = newBuilder(ident, icebergSchema);
     Transaction transaction =
         builder
-            .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
transforms))
-            .withLocation(properties.get("location"))
-            .withProperties(Spark3Util.rebuildCreateProperties(properties))
+            .withPartitionSpec(Spark3Util.toPartitionSpec(icebergSchema, 
tableInfo.partitions()))
+            .withLocation(tableInfo.properties().get("location"))
+            
.withProperties(Spark3Util.rebuildCreateProperties(tableInfo.properties()))
             .createOrReplaceTransaction();
     return new StagedSparkTable(transaction);
   }
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSchemaUtil.java 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSchemaUtil.java
index 9f299cb276..85583c8711 100644
--- 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSchemaUtil.java
+++ 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSchemaUtil.java
@@ -41,6 +41,7 @@ import org.apache.iceberg.types.Types;
 import org.apache.spark.sql.AnalysisException;
 import org.apache.spark.sql.SparkSession;
 import org.apache.spark.sql.catalog.Column;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.types.DataType;
 import org.apache.spark.sql.types.StructField;
 import org.apache.spark.sql.types.StructType;
@@ -108,6 +109,40 @@ public class SparkSchemaUtil {
     return TypeUtil.visit(type, new TypeToSparkType());
   }
 
+  /**
+   * Convert a Spark {@link TableInfo tableInfo} to a {@link Schema} with new 
field ids.
+   *
+   * <p>This conversion assigns fresh ids.
+   *
+   * <p>Default values are converted from spark default values.
+   *
+   * @param tableInfo a Spark TableInfo
+   * @return the equivalent Schema
+   * @throws IllegalArgumentException if the type cannot be converted
+   */
+  public static Schema convertWithDefaults(TableInfo tableInfo) {
+    Type converted =
+        SparkTypeVisitor.visit(tableInfo.schema(), new 
SparkTypeToType(tableInfo, true));
+    return new Schema(converted.asNestedType().asStructType().fields());
+  }
+
+  /**
+   * Convert a Spark {@link TableInfo tableInfo} to a {@link Schema} with new 
field ids.
+   *
+   * <p>This conversion assigns fresh ids.
+   *
+   * <p>Default values are ignored.
+   *
+   * @param tableInfo a Spark TableInfo
+   * @return the equivalent Schema
+   * @throws IllegalArgumentException if the type cannot be converted
+   */
+  public static Schema convert(TableInfo tableInfo) {
+    Type converted =
+        SparkTypeVisitor.visit(tableInfo.schema(), new 
SparkTypeToType(tableInfo, false));
+    return new Schema(converted.asNestedType().asStructType().fields());
+  }
+
   /**
    * Convert a Spark {@link StructType struct} to a {@link Schema} with new 
field ids.
    *
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSessionCatalog.java
 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSessionCatalog.java
index d754f84b27..d8a42eec97 100644
--- 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSessionCatalog.java
+++ 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkSessionCatalog.java
@@ -52,12 +52,11 @@ import 
org.apache.spark.sql.connector.catalog.SupportsNamespaces;
 import org.apache.spark.sql.connector.catalog.Table;
 import org.apache.spark.sql.connector.catalog.TableCatalog;
 import org.apache.spark.sql.connector.catalog.TableChange;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.connector.catalog.TableSummary;
 import org.apache.spark.sql.connector.catalog.View;
 import org.apache.spark.sql.connector.catalog.ViewCatalog;
 import org.apache.spark.sql.connector.catalog.functions.UnboundFunction;
-import org.apache.spark.sql.connector.expressions.Transform;
-import org.apache.spark.sql.types.StructType;
 import org.apache.spark.sql.util.CaseInsensitiveStringMap;
 
 /**
@@ -238,31 +237,29 @@ public class SparkSessionCatalog<
   }
 
   @Override
-  public Table createTable(
-      Identifier ident, StructType schema, Transform[] partitions, Map<String, 
String> properties)
+  public Table createTable(Identifier ident, TableInfo info)
       throws TableAlreadyExistsException, NoSuchNamespaceException {
     checkViewNotExists(ident);
 
-    String provider = properties.get("provider");
+    String provider = info.properties().get("provider");
     if (useIceberg(provider)) {
-      return icebergCatalog.createTable(ident, schema, partitions, properties);
+      return icebergCatalog.createTable(ident, info);
     } else {
       // delegate to the session catalog
-      return getSessionCatalog().createTable(ident, schema, partitions, 
properties);
+      return getSessionCatalog().createTable(ident, info);
     }
   }
 
   @Override
-  public StagedTable stageCreate(
-      Identifier ident, StructType schema, Transform[] partitions, Map<String, 
String> properties)
+  public StagedTable stageCreate(Identifier ident, TableInfo info)
       throws TableAlreadyExistsException, NoSuchNamespaceException {
     checkViewNotExists(ident);
 
-    String provider = properties.get("provider");
+    String provider = info.properties().get("provider");
     TableCatalog catalog;
     if (useIceberg(provider)) {
       if (asStagingCatalog != null) {
-        return asStagingCatalog.stageCreate(ident, schema, partitions, 
properties);
+        return asStagingCatalog.stageCreate(ident, info);
       }
       catalog = icebergCatalog;
     } else {
@@ -271,19 +268,18 @@ public class SparkSessionCatalog<
 
     // create the table with the session catalog, then wrap it in a staged 
table that will delete to
     // roll back
-    Table table = catalog.createTable(ident, schema, partitions, properties);
+    Table table = catalog.createTable(ident, info);
     return new RollbackStagedTable(catalog, ident, table);
   }
 
   @Override
-  public StagedTable stageReplace(
-      Identifier ident, StructType schema, Transform[] partitions, Map<String, 
String> properties)
+  public StagedTable stageReplace(Identifier ident, TableInfo info)
       throws NoSuchNamespaceException, NoSuchTableException {
-    String provider = properties.get("provider");
+    String provider = info.properties().get("provider");
     TableCatalog catalog;
     if (useIceberg(provider)) {
       if (asStagingCatalog != null) {
-        return asStagingCatalog.stageReplace(ident, schema, partitions, 
properties);
+        return asStagingCatalog.stageReplace(ident, info);
       }
       catalog = icebergCatalog;
     } else {
@@ -298,18 +294,17 @@ public class SparkSessionCatalog<
     try {
       // create the table with the session catalog, then wrap it in a staged 
table that will delete
       // to roll back
-      Table table = catalog.createTable(ident, schema, partitions, properties);
+      Table table = catalog.createTable(ident, info);
       return new RollbackStagedTable(catalog, ident, table);
 
     } catch (TableAlreadyExistsException e) {
       // the table was deleted, but now already exists again. retry the 
replace.
-      return stageReplace(ident, schema, partitions, properties);
+      return stageReplace(ident, info);
     }
   }
 
   @Override
-  public StagedTable stageCreateOrReplace(
-      Identifier ident, StructType schema, Transform[] partitions, Map<String, 
String> properties)
+  public StagedTable stageCreateOrReplace(Identifier ident, TableInfo info)
       throws NoSuchNamespaceException {
     if (viewExists(ident)) {
       // StagingTableCatalog does not declare a checked collision exception 
for create-or-replace.
@@ -317,11 +312,11 @@ public class SparkSessionCatalog<
           "Cannot create or replace table %s: a view with the same name 
already exists", ident);
     }
 
-    String provider = properties.get("provider");
+    String provider = info.properties().get("provider");
     TableCatalog catalog;
     if (useIceberg(provider)) {
       if (asStagingCatalog != null) {
-        return asStagingCatalog.stageCreateOrReplace(ident, schema, 
partitions, properties);
+        return asStagingCatalog.stageCreateOrReplace(ident, info);
       }
       catalog = icebergCatalog;
     } else {
@@ -334,12 +329,12 @@ public class SparkSessionCatalog<
     try {
       // create the table with the session catalog, then wrap it in a staged 
table that will delete
       // to roll back
-      Table sessionCatalogTable = catalog.createTable(ident, schema, 
partitions, properties);
+      Table sessionCatalogTable = catalog.createTable(ident, info);
       return new RollbackStagedTable(catalog, ident, sessionCatalogTable);
 
     } catch (TableAlreadyExistsException e) {
       // the table was deleted, but now already exists again. retry the 
replace.
-      return stageCreateOrReplace(ident, schema, partitions, properties);
+      return stageCreateOrReplace(ident, info);
     }
   }
 
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkTypeToType.java 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkTypeToType.java
index 6ce085a1b7..e658ebe1c0 100644
--- 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkTypeToType.java
+++ 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkTypeToType.java
@@ -20,10 +20,19 @@ package org.apache.iceberg.spark;
 
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.iceberg.expressions.Expressions;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.types.EdgeAlgorithm;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.Types;
+import org.apache.spark.sql.connector.catalog.Column;
+import org.apache.spark.sql.connector.catalog.ColumnDefaultValue;
+import org.apache.spark.sql.connector.catalog.TableInfo;
+import org.apache.spark.sql.connector.expressions.Literal;
 import org.apache.spark.sql.types.ArrayType;
 import org.apache.spark.sql.types.BinaryType;
 import org.apache.spark.sql.types.BooleanType;
@@ -52,6 +61,8 @@ import org.apache.spark.sql.types.VariantType;
 
 class SparkTypeToType extends SparkTypeVisitor<Type> {
   private final StructType root;
+  private boolean includeDefaults;
+  private Map<String, Column> nameToColumnMap = Map.of();
   private int nextId = 0;
 
   SparkTypeToType() {
@@ -64,6 +75,14 @@ class SparkTypeToType extends SparkTypeVisitor<Type> {
     this.nextId = root.fields().length;
   }
 
+  SparkTypeToType(TableInfo tableInfo, boolean includeDefaults) {
+    this(tableInfo.schema());
+
+    this.includeDefaults = includeDefaults;
+    this.nameToColumnMap =
+        Stream.of(tableInfo.columns()).collect(Collectors.toMap(Column::name, 
Function.identity()));
+  }
+
   private int getNextId() {
     int next = nextId;
     nextId += 1;
@@ -71,11 +90,10 @@ class SparkTypeToType extends SparkTypeVisitor<Type> {
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
   public Type struct(StructType struct, List<Type> types) {
     StructField[] fields = struct.fields();
     List<Types.NestedField> newFields = 
Lists.newArrayListWithExpectedSize(fields.length);
-    boolean isRoot = root == struct;
+    boolean isRoot = root != null && root.equals(struct);
     for (int i = 0; i < fields.length; i += 1) {
       StructField field = fields[i];
       Type type = types.get(i);
@@ -88,13 +106,23 @@ class SparkTypeToType extends SparkTypeVisitor<Type> {
         id = getNextId();
       }
 
-      String doc = field.getComment().isDefined() ? field.getComment().get() : 
null;
+      Types.NestedField.Builder fieldBuilder =
+          Types.NestedField.builder()
+              .isOptional(field.nullable())
+              .withId(id)
+              .withName(field.name())
+              .ofType(type);
 
-      if (field.nullable()) {
-        newFields.add(Types.NestedField.optional(id, field.name(), type, doc));
-      } else {
-        newFields.add(Types.NestedField.required(id, field.name(), type, doc));
+      if (field.getComment().isDefined()) {
+        fieldBuilder.withDoc(field.getComment().get());
+      }
+
+      // spark only supports defaults at the top-level
+      if (isRoot && includeDefaults) {
+        convertDefaultValue(fieldBuilder, field);
       }
+
+      newFields.add(fieldBuilder.build());
     }
 
     return Types.StructType.of(newFields);
@@ -201,4 +229,42 @@ class SparkTypeToType extends SparkTypeVisitor<Type> {
             "Iceberg does not support Spark geography edge algorithm: " + 
algorithm);
     }
   }
+
+  private void convertDefaultValue(Types.NestedField.Builder icebergField, 
StructField sparkField) {
+    Column column = nameToColumnMap.get(sparkField.name());
+
+    if (column == null) {
+      return;
+    }
+
+    ColumnDefaultValue columnDefaultValue = column.defaultValue();
+
+    if (columnDefaultValue == null) {
+      return;
+    }
+
+    if (columnDefaultValue.getExpression() == null && 
columnDefaultValue.getSql() != null) {
+      throw new UnsupportedOperationException(
+          "Unsupported default value expression: " + 
columnDefaultValue.getSql());
+    }
+
+    if (columnDefaultValue.getExpression() != null
+        && !SparkV2Filters.isLiteral(columnDefaultValue.getExpression())) {
+      throw new UnsupportedOperationException(
+          "Default value expressions are not supported in Iceberg");
+    }
+
+    // ColumnDefaultValue.getValue is equivalent to initial-default in Iceberg
+    Literal<?> initialDefault = columnDefaultValue.getValue();
+    if (initialDefault != null && initialDefault.value() != null) {
+      icebergField.withInitialDefault(
+          Expressions.lit(SparkV2Filters.convertLiteral(initialDefault)));
+    }
+
+    // ColumnDefaultValue.getExpression() is equivalent to write-default in 
Iceberg
+    Literal<?> writeDefault = (Literal<?>) columnDefaultValue.getExpression();
+    if (writeDefault != null && writeDefault.value() != null) {
+      
icebergField.withWriteDefault(Expressions.lit(SparkV2Filters.convertLiteral(writeDefault)));
+    }
+  }
 }
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkV2Filters.java 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkV2Filters.java
index 57b9d61e38..739d4e1caf 100644
--- 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkV2Filters.java
+++ 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/SparkV2Filters.java
@@ -372,15 +372,19 @@ public class SparkV2Filters {
     return false;
   }
 
-  private static boolean 
isLiteral(org.apache.spark.sql.connector.expressions.Expression expr) {
+  static boolean 
isLiteral(org.apache.spark.sql.connector.expressions.Expression expr) {
     return expr instanceof Literal;
   }
 
-  private static Object convertLiteral(Literal<?> literal) {
+  static Object convertLiteral(Literal<?> literal) {
     if (literal.value() instanceof UTF8String) {
       return ((UTF8String) literal.value()).toString();
     } else if (literal.value() instanceof Decimal) {
       return ((Decimal) literal.value()).toJavaBigDecimal();
+    } else if (literal.value() instanceof Short) {
+      return ((Short) literal.value()).intValue();
+    } else if (literal.value() instanceof Byte) {
+      return ((Byte) literal.value()).intValue();
     }
     return literal.value();
   }
diff --git 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/actions/BaseTableCreationSparkAction.java
 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/actions/BaseTableCreationSparkAction.java
index 37e1ec4ce7..574bed18de 100644
--- 
a/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/actions/BaseTableCreationSparkAction.java
+++ 
b/spark/v4.2/spark/src/main/java/org/apache/iceberg/spark/actions/BaseTableCreationSparkAction.java
@@ -45,6 +45,7 @@ import org.apache.spark.sql.connector.catalog.CatalogPlugin;
 import org.apache.spark.sql.connector.catalog.Identifier;
 import org.apache.spark.sql.connector.catalog.StagingTableCatalog;
 import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.connector.catalog.V1Table;
 import org.apache.spark.sql.connector.expressions.Transform;
 import org.apache.spark.sql.types.StructType;
@@ -158,8 +159,13 @@ abstract class BaseTableCreationSparkAction<ThisT> extends 
BaseSparkAction<ThisT
       Map<String, String> props = destTableProps();
       StructType schema = sourceTable.schema();
       Transform[] partitioning = sourceTable.partitioning();
-      return (StagedSparkTable)
-          destCatalog().stageCreate(destTableIdent(), schema, partitioning, 
props);
+      TableInfo tableInfo =
+          new TableInfo.Builder()
+              .withSchema(schema)
+              .withPartitions(partitioning)
+              .withProperties(props)
+              .build();
+      return (StagedSparkTable) destCatalog().stageCreate(destTableIdent(), 
tableInfo);
     } catch (org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException 
e) {
       throw new NoSuchNamespaceException(
           "Cannot create table %s as the namespace does not exist", 
destTableIdent());
diff --git 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSessionCatalog.java
 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSessionCatalog.java
index 46897c2f9a..4438e22cf5 100644
--- 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSessionCatalog.java
+++ 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/TestSparkSessionCatalog.java
@@ -43,6 +43,7 @@ import 
org.apache.spark.sql.connector.catalog.SupportsDeleteV2;
 import org.apache.spark.sql.connector.catalog.SupportsNamespaces;
 import org.apache.spark.sql.connector.catalog.Table;
 import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.connector.catalog.TableSummary;
 import org.apache.spark.sql.connector.catalog.View;
 import org.apache.spark.sql.connector.catalog.ViewCatalog;
@@ -390,17 +391,26 @@ public class TestSparkSessionCatalog extends TestBase {
     SparkSessionCatalog<?> catalog = catalogWithViews(icebergCatalog, 
sessionCatalog);
     StructType schema = new StructType();
     Transform[] partitions = new Transform[0];
-
-    assertThatThrownBy(() -> catalog.createTable(ident, schema, partitions, 
Collections.emptyMap()))
+    TableInfo tableInfo =
+        new TableInfo.Builder()
+            .withSchema(schema)
+            .withPartitions(partitions)
+            .withProperties(Collections.emptyMap())
+            .build();
+    TableInfo parquetTableInfo =
+        new TableInfo.Builder()
+            .withSchema(schema)
+            .withPartitions(partitions)
+            .withProperties(Collections.singletonMap("provider", "parquet"))
+            .build();
+
+    assertThatThrownBy(() -> catalog.createTable(ident, tableInfo))
         .isInstanceOf(TableAlreadyExistsException.class)
         .hasMessageContaining(ident.name());
-    assertThatThrownBy(() -> catalog.stageCreate(ident, schema, partitions, 
Collections.emptyMap()))
+    assertThatThrownBy(() -> catalog.stageCreate(ident, tableInfo))
         .isInstanceOf(TableAlreadyExistsException.class)
         .hasMessageContaining(ident.name());
-    assertThatThrownBy(
-            () ->
-                catalog.stageCreateOrReplace(
-                    ident, schema, partitions, 
Collections.singletonMap("provider", "parquet")))
+    assertThatThrownBy(() -> catalog.stageCreateOrReplace(ident, 
parquetTableInfo))
         .isInstanceOf(AlreadyExistsException.class)
         .hasMessage(
             "Cannot create or replace table %s: a view with the same name 
already exists", ident);
diff --git 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRemoveOrphanFilesAction3.java
 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRemoveOrphanFilesAction3.java
index 88ac800b15..4a46ca1a00 100644
--- 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRemoveOrphanFilesAction3.java
+++ 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRemoveOrphanFilesAction3.java
@@ -28,6 +28,7 @@ import org.apache.iceberg.spark.SparkSchemaUtil;
 import org.apache.iceberg.spark.SparkSessionCatalog;
 import org.apache.iceberg.spark.source.SparkTable;
 import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.connector.expressions.Transform;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.TestTemplate;
@@ -43,7 +44,13 @@ public class TestRemoveOrphanFilesAction3 extends 
TestRemoveOrphanFilesAction {
     String[] database = {"default"};
     Identifier id = Identifier.of(database, randomName("table"));
     Transform[] transforms = {};
-    cat.createTable(id, SparkSchemaUtil.convert(SCHEMA), transforms, 
properties);
+    TableInfo tableInfo =
+        new TableInfo.Builder()
+            .withSchema(SparkSchemaUtil.convert(SCHEMA))
+            .withPartitions(transforms)
+            .withProperties(properties)
+            .build();
+    cat.createTable(id, tableInfo);
     SparkTable table = (SparkTable) cat.loadTable(id);
 
     sql("INSERT INTO mycat.default.%s VALUES (1,1,1)", id.name());
@@ -73,7 +80,13 @@ public class TestRemoveOrphanFilesAction3 extends 
TestRemoveOrphanFilesAction {
     String[] database = {"default"};
     Identifier id = Identifier.of(database, randomName("table"));
     Transform[] transforms = {};
-    cat.createTable(id, SparkSchemaUtil.convert(SCHEMA), transforms, 
properties);
+    TableInfo tableInfo =
+        new TableInfo.Builder()
+            .withSchema(SparkSchemaUtil.convert(SCHEMA))
+            .withPartitions(transforms)
+            .withProperties(properties)
+            .build();
+    cat.createTable(id, tableInfo);
     SparkTable table = (SparkTable) cat.loadTable(id);
 
     sql("INSERT INTO hadoop.default.%s VALUES (1,1,1)", id.name());
@@ -103,7 +116,13 @@ public class TestRemoveOrphanFilesAction3 extends 
TestRemoveOrphanFilesAction {
     String[] database = {"default"};
     Identifier id = Identifier.of(database, randomName("table"));
     Transform[] transforms = {};
-    cat.createTable(id, SparkSchemaUtil.convert(SCHEMA), transforms, 
properties);
+    TableInfo tableInfo =
+        new TableInfo.Builder()
+            .withSchema(SparkSchemaUtil.convert(SCHEMA))
+            .withPartitions(transforms)
+            .withProperties(properties)
+            .build();
+    cat.createTable(id, tableInfo);
     SparkTable table = (SparkTable) cat.loadTable(id);
 
     sql("INSERT INTO hive.default.%s VALUES (1,1,1)", id.name());
@@ -137,7 +156,13 @@ public class TestRemoveOrphanFilesAction3 extends 
TestRemoveOrphanFilesAction {
     String[] database = {"default"};
     Identifier id = Identifier.of(database, randomName("table"));
     Transform[] transforms = {};
-    cat.createTable(id, SparkSchemaUtil.convert(SCHEMA), transforms, 
properties);
+    TableInfo tableInfo =
+        new TableInfo.Builder()
+            .withSchema(SparkSchemaUtil.convert(SCHEMA))
+            .withPartitions(transforms)
+            .withProperties(properties)
+            .build();
+    cat.createTable(id, tableInfo);
     SparkTable table = (SparkTable) cat.loadTable(id);
 
     sql("INSERT INTO default.%s VALUES (1,1,1)", id.name());
@@ -170,7 +195,13 @@ public class TestRemoveOrphanFilesAction3 extends 
TestRemoveOrphanFilesAction {
     Identifier id = Identifier.of(database, "sessioncattest");
     Transform[] transforms = {};
     cat.dropTable(id);
-    cat.createTable(id, SparkSchemaUtil.convert(SCHEMA), transforms, 
properties);
+    TableInfo tableInfo =
+        new TableInfo.Builder()
+            .withSchema(SparkSchemaUtil.convert(SCHEMA))
+            .withPartitions(transforms)
+            .withProperties(properties)
+            .build();
+    cat.createTable(id, tableInfo);
     SparkTable table = (SparkTable) cat.loadTable(id);
 
     sql("INSERT INTO default.sessioncattest VALUES (1,1,1)");
diff --git 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestPathIdentifier.java
 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestPathIdentifier.java
index aa8fe047b3..b244f71b8a 100644
--- 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestPathIdentifier.java
+++ 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestPathIdentifier.java
@@ -33,9 +33,10 @@ import org.apache.iceberg.spark.SparkCatalog;
 import org.apache.iceberg.spark.SparkSchemaUtil;
 import org.apache.iceberg.spark.TestBase;
 import org.apache.iceberg.types.Types;
+import org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException;
 import org.apache.spark.sql.catalyst.analysis.NoSuchTableException;
 import org.apache.spark.sql.catalyst.analysis.TableAlreadyExistsException;
-import org.apache.spark.sql.connector.expressions.Transform;
+import org.apache.spark.sql.connector.catalog.TableInfo;
 import org.apache.spark.sql.util.CaseInsensitiveStringMap;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -67,11 +68,12 @@ public class TestPathIdentifier extends TestBase {
   }
 
   @Test
-  public void testPathIdentifier() throws TableAlreadyExistsException, 
NoSuchTableException {
-    SparkTable table =
-        (SparkTable)
-            sparkCatalog.createTable(
-                identifier, SparkSchemaUtil.convert(SCHEMA), new Transform[0], 
ImmutableMap.of());
+  public void testPathIdentifier()
+      throws TableAlreadyExistsException, NoSuchTableException, 
NoSuchNamespaceException {
+    TableInfo tableInfo =
+        new 
TableInfo.Builder().withSchema(SparkSchemaUtil.convert(SCHEMA)).build();
+
+    SparkTable table = (SparkTable) sparkCatalog.createTable(identifier, 
tableInfo);
 
     
assertThat(tableLocation.getAbsolutePath()).isEqualTo(table.table().location());
     assertThat(table.table()).isInstanceOf(BaseTable.class);
diff --git 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/sql/TestSparkDefaultValues.java
 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/sql/TestSparkDefaultValues.java
index 838e735c75..fa6c1ec03c 100644
--- 
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/sql/TestSparkDefaultValues.java
+++ 
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/sql/TestSparkDefaultValues.java
@@ -21,29 +21,30 @@ package org.apache.iceberg.spark.sql;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.nio.ByteBuffer;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.HexFormat;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.expressions.Literal;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.spark.CatalogTestBase;
 import org.apache.iceberg.types.Types;
-import org.apache.spark.sql.AnalysisException;
+import org.apache.iceberg.util.DateTimeUtil;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.TestTemplate;
 
-/**
- * Tests for Spark SQL Default values integration with Iceberg default values.
- *
- * <p>Note: These tests use {@code validationCatalog.createTable()} to create 
tables with default
- * values because the Iceberg Spark integration does not yet support default 
value clauses in Spark
- * DDL.
- */
+/** Tests for Spark SQL Default values integration with Iceberg default 
values. */
 public class TestSparkDefaultValues extends CatalogTestBase {
 
   @AfterEach
   public void dropTestTable() {
     sql("DROP TABLE IF EXISTS %s", tableName);
+    sql("DROP TABLE IF EXISTS %s", tableName("dest"));
   }
 
   @TestTemplate
@@ -141,7 +142,104 @@ public class TestSparkDefaultValues extends 
CatalogTestBase {
   }
 
   @TestTemplate
-  public void testCreateTableWithDefaultsUnsupported() {
+  public void testCreateTableWithDefaults() {
+    assertThat(validationCatalog.tableExists(tableIdent))
+        .as("Table should not already exist")
+        .isFalse();
+
+    sql(
+        "CREATE TABLE %s (id INT DEFAULT -1, data STRING DEFAULT '-') USING 
iceberg "
+            + "TBLPROPERTIES ('format-version'='3')",
+        tableName);
+
+    Types.NestedField idField = 
validationCatalog.loadTable(tableIdent).schema().findField("id");
+    assertThat(idField.initialDefault()).isEqualTo(-1);
+    assertThat(idField.writeDefault()).isEqualTo(-1);
+
+    Types.NestedField dataField =
+        validationCatalog.loadTable(tableIdent).schema().findField("data");
+    assertThat(dataField.initialDefault()).isEqualTo("-");
+    assertThat(dataField.writeDefault()).isEqualTo("-");
+  }
+
+  @TestTemplate
+  public void testCreateTableWithBoxedDefaults() {
+    assertThat(validationCatalog.tableExists(tableIdent))
+        .as("Table should not already exist")
+        .isFalse();
+
+    sql(
+        "CREATE TABLE %s (ti TINYINT DEFAULT -1, s SHORT DEFAULT '-2') USING 
iceberg "
+            + "TBLPROPERTIES ('format-version'='3')",
+        tableName);
+
+    Types.NestedField tinyField = 
validationCatalog.loadTable(tableIdent).schema().findField("ti");
+    assertThat(tinyField.initialDefault()).isEqualTo(-1);
+    assertThat(tinyField.writeDefault()).isEqualTo(-1);
+
+    Types.NestedField shortField = 
validationCatalog.loadTable(tableIdent).schema().findField("s");
+    assertThat(shortField.initialDefault()).isEqualTo(-2);
+    assertThat(shortField.writeDefault()).isEqualTo(-2);
+  }
+
+  @TestTemplate
+  public void testCreateTableWithBinaryDefaults() {
+    assertThat(validationCatalog.tableExists(tableIdent))
+        .as("Table should not already exist")
+        .isFalse();
+
+    sql(
+        "CREATE TABLE %s (b BINARY DEFAULT X'1ABF') USING iceberg "
+            + "TBLPROPERTIES ('format-version'='3')",
+        tableName);
+
+    ByteBuffer expected = ByteBuffer.wrap(HexFormat.of().parseHex("1ABF"));
+
+    Types.NestedField binaryField = 
validationCatalog.loadTable(tableIdent).schema().findField("b");
+    assertThat(binaryField.initialDefault()).isEqualTo(expected);
+    assertThat(binaryField.writeDefault()).isEqualTo(expected);
+  }
+
+  @TestTemplate
+  public void testCreateTableWithTimestampDefaults() {
+    assertThat(validationCatalog.tableExists(tableIdent))
+        .as("Table should not already exist")
+        .isFalse();
+
+    sql(
+        "CREATE TABLE %s (ts TIMESTAMP DEFAULT timestamp '2026-09-24 
21:26:02.98269') USING iceberg "
+            + "TBLPROPERTIES ('format-version'='3')",
+        tableName);
+
+    long expectedMicros =
+        DateTimeUtil.microsFromInstant(
+            LocalDateTime.parse("2026-09-24T21:26:02.98269")
+                
.atZone(ZoneId.of(spark.sessionState().conf().sessionLocalTimeZone()))
+                .toInstant());
+
+    Types.NestedField tsField = 
validationCatalog.loadTable(tableIdent).schema().findField("ts");
+    assertThat(tsField.initialDefault()).isEqualTo(expectedMicros);
+    assertThat(tsField.writeDefault()).isEqualTo(expectedMicros);
+  }
+
+  @TestTemplate
+  public void testCreateTableWithNullDefaults() {
+    assertThat(validationCatalog.tableExists(tableIdent))
+        .as("Table should not already exist")
+        .isFalse();
+
+    sql(
+        "CREATE TABLE %s (s String DEFAULT null) USING iceberg "
+            + "TBLPROPERTIES ('format-version'='3')",
+        tableName);
+
+    Types.NestedField stringField = 
validationCatalog.loadTable(tableIdent).schema().findField("s");
+    assertThat(stringField.initialDefault()).isNull();
+    assertThat(stringField.writeDefault()).isNull();
+  }
+
+  @TestTemplate
+  public void testCreateTableWithDefaultsExpressionsUnsupported() {
     assertThat(validationCatalog.tableExists(tableIdent))
         .as("Table should not already exist")
         .isFalse();
@@ -149,10 +247,10 @@ public class TestSparkDefaultValues extends 
CatalogTestBase {
     assertThatThrownBy(
             () ->
                 sql(
-                    "CREATE TABLE %s (id INT, data STRING DEFAULT 
'default-value') USING iceberg",
+                    "CREATE TABLE %s (id INT, data STRING DEFAULT 
CURRENT_USER) USING iceberg",
                     tableName))
-        .isInstanceOf(AnalysisException.class)
-        .hasMessageContaining("does not support column default value");
+        .isInstanceOf(UnsupportedOperationException.class)
+        .hasMessageContaining("Unsupported default value expression: 
CURRENT_USER");
   }
 
   @TestTemplate
@@ -172,6 +270,62 @@ public class TestSparkDefaultValues extends 
CatalogTestBase {
         .hasMessageContaining("default values in Spark is currently 
unsupported");
   }
 
+  @TestTemplate
+  public void testCtasExcludesDefault() {
+    assertThat(validationCatalog.tableExists(tableIdent))
+        .as("Table should not already exist")
+        .isFalse();
+
+    sql(
+        "CREATE TABLE %s (data STRING DEFAULT '-') USING iceberg "
+            + "TBLPROPERTIES ('format-version'='3')",
+        tableName);
+
+    sql("INSERT INTO %s VALUES (DEFAULT)", tableName);
+
+    sql("CREATE TABLE %s USING iceberg AS SELECT * FROM %s", 
tableName("dest"), tableName);
+
+    TableIdentifier destIdent = TableIdentifier.of(Namespace.of("default"), 
"dest");
+    Types.NestedField dataField = 
validationCatalog.loadTable(destIdent).schema().findField("data");
+    assertThat(dataField.initialDefault()).isNull();
+    assertThat(dataField.writeDefault()).isNull();
+
+    assertEquals(
+        "Should have correct default values from source table",
+        ImmutableList.of(row("-")),
+        sql("SELECT * FROM %s", tableName("dest")));
+  }
+
+  @TestTemplate
+  public void testRtasExcludesDefault() {
+    assertThat(validationCatalog.tableExists(tableIdent))
+        .as("Table should not already exist")
+        .isFalse();
+
+    sql(
+        "CREATE TABLE %s (data STRING DEFAULT '-') USING iceberg "
+            + "TBLPROPERTIES ('format-version'='3')",
+        tableName);
+
+    sql("INSERT INTO %s VALUES (DEFAULT)", tableName);
+
+    sql(
+        "CREATE TABLE %s (data STRING DEFAULT '-') USING iceberg TBLPROPERTIES 
('format-version'='3')",
+        tableName("dest"));
+
+    sql("REPLACE TABLE %s USING iceberg AS SELECT * FROM %s", 
tableName("dest"), tableName);
+
+    TableIdentifier destIdent = TableIdentifier.of(Namespace.of("default"), 
"dest");
+    Types.NestedField dataField = 
validationCatalog.loadTable(destIdent).schema().findField("data");
+    assertThat(dataField.initialDefault()).isNull();
+    assertThat(dataField.writeDefault()).isNull();
+
+    assertEquals(
+        "Should have correct default values from source table",
+        ImmutableList.of(row("-")),
+        sql("SELECT * FROM %s", tableName("dest")));
+  }
+
   @TestTemplate
   public void testSchemaEvolutionWithDefaultValueChanges() {
     assertThat(validationCatalog.tableExists(tableIdent))

Reply via email to