Repository: duckdb/duckdb (2026-07-28T06:24:52Z)

Issue:
Struct field with default value doesn't gets updated after rename
### What happens?

See repro for details.

### To Reproduce

```sql
memory D CREATE TABLE t(s STRUCT(a INT, b INT) DEFAULT {'a':1,'b':2});
memory D INSERT INTO t DEFAULT VALUES;
memory D ALTER TABLE t RENAME COLUMN s.a TO c;
memory D SELECT s FROM t;
┌──────────────────────────────┐
│              s               │
│ struct(c integer, b integer) │
├──────────────────────────────┤
│ {'c': 1, 'b': 2}             │
└──────────────────────────────┘
memory D INSERT INTO t DEFAULT VALUES;

-- both rows should be {'c': 1, 'b': 2}
memory D SELECT s FROM t;
┌──────────────────────────────┐
│              s               │
│ struct(c integer, b integer) │
├──────────────────────────────┤
│ {'c': 1, 'b': 2}             │
│ {'c': NULL, 'b': 2}          │
└──────────────────────────────┘

-- the struct_pack function argument is obviously incorrect
memory D SELECT sql FROM duckdb_tables() WHERE table_name='t';
┌──────────────────────────────────────────────────────────────────────────────────────┐
│                                         sql                                          │
│                                       varchar                                        │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ CREATE TABLE t(s STRUCT(c INTEGER, b INTEGER) DEFAULT(struct_pack(a := 1, b := 2))); │
└──────────────────────────────────────────────────────────────────────────────────────┘
```

### OS:

macos

### DuckDB Version:

531911608719cd03b723051b8cb3b3e81cf3e03b

### DuckDB Client:

SQL

### Hardware:

_No response_

### Full Name:

dentiny

### Affiliation:

N/A

### Did you include all relevant configuration (e.g., CPU architecture, Linux distribution) to reproduce the issue?

- [x] Yes, I have

### Did you include all code required to reproduce the issue?

- [x] Yes, I have

### Did you include all relevant data sets for reproducing the issue?

Yes


Discussion:

Patch:
diff --git a/src/catalog/catalog_entry/duck_table_entry.cpp b/src/catalog/catalog_entry/duck_table_entry.cpp
index d6b0fb105e96..ebf418354235 100644
--- a/src/catalog/catalog_entry/duck_table_entry.cpp
+++ b/src/catalog/catalog_entry/duck_table_entry.cpp
@@ -10,6 +10,7 @@
 #include "duckdb/function/table/table_scan.hpp"
 #include "duckdb/main/database.hpp"
 #include "duckdb/parser/constraints/list.hpp"
+#include "duckdb/parser/expression/constant_expression.hpp"
 #include "duckdb/parser/parsed_data/comment_on_column_info.hpp"
 #include "duckdb/parser/parsed_expression_iterator.hpp"
 #include "duckdb/planner/binder.hpp"
@@ -333,7 +334,7 @@ unique_ptr<CatalogEntry> DuckTableEntry::AlterEntry(ClientContext &context, Alte
 	}
 	case AlterTableType::ALTER_COLUMN_TYPE: {
 		auto &change_type_info = table_info.Cast<ChangeColumnTypeInfo>();
-		return ChangeColumnType(context, change_type_info);
+		return ChangeColumnType(context, change_type_info, AlterTableType::ALTER_COLUMN_TYPE);
 	}
 	case AlterTableType::FOREIGN_KEY_CONSTRAINT: {
 		auto &foreign_key_constraint_info = table_info.Cast<AlterForeignKeyInfo>();
@@ -391,6 +392,29 @@ static void RenameExpression(ParsedExpression &root_expr, RenameColumnInfo &info
 	});
 }
 
+// Keep struct literal defaults aligned with nested field renames.
+static unique_ptr<ParsedExpression> RemapStructDefault(unique_ptr<ParsedExpression> default_value,
+                                                       const LogicalType &new_type, const Value &mapping) {
+	vector<unique_ptr<ParsedExpression>> children;
+	children.push_back(std::move(default_value));
+	children.push_back(make_uniq<ConstantExpression>(Value(new_type)));
+	children.push_back(make_uniq<ConstantExpression>(mapping.Copy()));
+	children.push_back(make_uniq<ConstantExpression>(Value()));
+	return make_uniq<FunctionExpression>("remap_struct", std::move(children));
+}
+
+static const Value &GetRemapStructMapping(ChangeColumnTypeInfo &info) {
+	D_ASSERT(info.expression);
+	D_ASSERT(info.expression->GetExpressionClass() == ExpressionClass::FUNCTION);
+	auto &function = info.expression->Cast<FunctionExpression>();
+	D_ASSERT(function.FunctionName() == "remap_struct");
+	auto &arguments = function.GetArguments();
+	D_ASSERT(arguments.size() == 4);
+	auto &mapping = arguments[2].GetExpression();
+	D_ASSERT(mapping.GetExpressionClass() == ExpressionClass::CONSTANT);
+	return mapping.Cast<ConstantExpression>().GetValue();
+}
+
 unique_ptr<CatalogEntry> DuckTableEntry::RenameColumn(ClientContext &context, RenameColumnInfo &info) {
 	auto rename_idx = GetColumnIndex(info.old_name);
 	if (rename_idx.index == COLUMN_IDENTIFIER_ROW_ID) {
@@ -674,7 +698,7 @@ unique_ptr<CatalogEntry> DuckTableEntry::AddField(ClientContext &context, AddFie
 
 	ChangeColumnTypeInfo change_column_type(info.GetAlterEntryData(), info.column_path[0], std::move(res.new_type),
 	                                        std::move(function));
-	return ChangeColumnType(context, change_column_type);
+	return ChangeColumnType(context, change_column_type, AlterTableType::ADD_FIELD);
 }
 
 void DuckTableEntry::UpdateConstraintsOnColumnDrop(const LogicalIndex &removed_index,
@@ -930,7 +954,7 @@ unique_ptr<CatalogEntry> DuckTableEntry::RemoveField(ClientContext &context, Rem
 
 	ChangeColumnTypeInfo change_column_type(info.GetAlterEntryData(), info.column_path[0], std::move(res.new_type),
 	                                        std::move(function));
-	return ChangeColumnType(context, change_column_type);
+	return ChangeColumnType(context, change_column_type, AlterTableType::REMOVE_FIELD);
 }
 
 DroppedFieldMapping RenameFieldFromStruct(const LogicalType &type, const vector<Identifier> &column_path,
@@ -1024,7 +1048,7 @@ unique_ptr<CatalogEntry> DuckTableEntry::RenameField(ClientContext &context, Ren
 	auto function = make_uniq<FunctionExpression>("remap_struct", std::move(children));
 	ChangeColumnTypeInfo change_column_type(info.GetAlterEntryData(), info.column_path[0], std::move(res.new_type),
 	                                        std::move(function));
-	return ChangeColumnType(context, change_column_type);
+	return ChangeColumnType(context, change_column_type, AlterTableType::RENAME_FIELD);
 }
 
 unique_ptr<CatalogEntry> DuckTableEntry::SetDefault(ClientContext &context, SetDefaultInfo &info) {
@@ -1112,7 +1136,8 @@ unique_ptr<CatalogEntry> DuckTableEntry::DropNotNull(ClientContext &context, Dro
 	return make_uniq<DuckTableEntry>(catalog, schema, *bound_create_info, storage, triggers);
 }
 
-unique_ptr<CatalogEntry> DuckTableEntry::ChangeColumnType(ClientContext &context, ChangeColumnTypeInfo &info) {
+unique_ptr<CatalogEntry> DuckTableEntry::ChangeColumnType(ClientContext &context, ChangeColumnTypeInfo &info,
+                                                          AlterTableType alter_table_type) {
 	// Bind type
 	auto type_binder = Binder::CreateBinder(context);
 	type_binder->SetSearchPath(catalog, schema.name);
@@ -1148,6 +1173,10 @@ unique_ptr<CatalogEntry> DuckTableEntry::ChangeColumnType(ClientContext &context
 				throw NotImplementedException("Changing types of generated columns is not supported yet");
 			}
 			copy.SetType(info.target_type);
+			if (alter_table_type == AlterTableType::RENAME_FIELD && copy.HasDefaultValue()) {
+				copy.SetDefaultValue(
+				    RemapStructDefault(copy.DefaultValue().Copy(), info.target_type, GetRemapStructMapping(info)));
+			}
 		}
 		// TODO: check if the generated_expression breaks, only delete it if it does
 		if (copy.Generated() && column_dependency_manager.IsDependencyOf(col.Logical(), change_idx)) {
diff --git a/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp b/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp
index 0ebc3f35555b..d17d42baec4e 100644
--- a/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp
+++ b/src/include/duckdb/catalog/catalog_entry/duck_table_entry.hpp
@@ -85,7 +85,8 @@ class DuckTableEntry : public TableCatalogEntry {
 	unique_ptr<CatalogEntry> RemoveColumn(ClientContext &context, RemoveColumnInfo &info);
 	unique_ptr<CatalogEntry> RemoveField(ClientContext &context, RemoveFieldInfo &info);
 	unique_ptr<CatalogEntry> SetDefault(ClientContext &context, SetDefaultInfo &info);
-	unique_ptr<CatalogEntry> ChangeColumnType(ClientContext &context, ChangeColumnTypeInfo &info);
+	unique_ptr<CatalogEntry> ChangeColumnType(ClientContext &context, ChangeColumnTypeInfo &info,
+	                                          AlterTableType alter_table_type);
 	unique_ptr<CatalogEntry> SetNotNull(ClientContext &context, SetNotNullInfo &info);
 	unique_ptr<CatalogEntry> DropNotNull(ClientContext &context, DropNotNullInfo &info);
 	unique_ptr<CatalogEntry> AddForeignKeyConstraint(AlterForeignKeyInfo &info);


Test patch:
diff --git a/test/sql/alter/struct/rename_col_nested_struct.test b/test/sql/alter/struct/rename_col_nested_struct.test
index edae1d2d768d..ad62652c8508 100644
--- a/test/sql/alter/struct/rename_col_nested_struct.test
+++ b/test/sql/alter/struct/rename_col_nested_struct.test
@@ -34,3 +34,41 @@ SELECT * FROM test
 ----
 {'x': {'i': 1, 'v2': 1}}
 {'x': {'i': 2, 'v2': 2}}
+
+# rename updates nested struct default field names
+statement ok
+CREATE TABLE test_default(s STRUCT(s2 STRUCT(a INTEGER, b INTEGER)) DEFAULT {'s2': {'a': 1, 'b': 2}})
+
+statement ok
+INSERT INTO test_default DEFAULT VALUES
+
+statement ok
+ALTER TABLE test_default RENAME s.s2.a TO c
+
+statement ok
+INSERT INTO test_default DEFAULT VALUES
+
+query I
+SELECT s FROM test_default
+----
+{'s2': {'c': 1, 'b': 2}}
+{'s2': {'c': 1, 'b': 2}}
+
+# rename updates intermediate struct default field names
+statement ok
+CREATE TABLE test_default_parent(s STRUCT(s2 STRUCT(a INTEGER, b INTEGER)) DEFAULT {'s2': {'a': 1, 'b': 2}})
+
+statement ok
+INSERT INTO test_default_parent DEFAULT VALUES
+
+statement ok
+ALTER TABLE test_default_parent RENAME s.s2 TO x
+
+statement ok
+INSERT INTO test_default_parent DEFAULT VALUES
+
+query I
+SELECT s FROM test_default_parent
+----
+{'x': {'a': 1, 'b': 2}}
+{'x': {'a': 1, 'b': 2}}
diff --git a/test/sql/alter/struct/rename_col_struct.test b/test/sql/alter/struct/rename_col_struct.test
index 819e93fd51bf..060ce1963a67 100644
--- a/test/sql/alter/struct/rename_col_struct.test
+++ b/test/sql/alter/struct/rename_col_struct.test
@@ -58,3 +58,40 @@ statement error
 ALTER TABLE test RENAME x.i TO v2
 ----
 does not exist
+
+# rename updates struct default field names
+statement ok
+CREATE TABLE test_default(s STRUCT(a INTEGER, b INTEGER) DEFAULT {'a': 1, 'b': 2})
+
+statement ok
+INSERT INTO test_default DEFAULT VALUES
+
+statement ok
+ALTER TABLE test_default RENAME s.a TO c
+
+statement ok
+INSERT INTO test_default DEFAULT VALUES
+
+query I
+SELECT s FROM test_default
+----
+{'c': 1, 'b': 2}
+{'c': 1, 'b': 2}
+
+# rename updates struct default field names through a cast
+statement ok
+CREATE TABLE test_cast_default(
+    s STRUCT(a INTEGER, b INTEGER)
+    DEFAULT (struct_pack(a := 1, b := 2)::STRUCT(a INTEGER, b INTEGER))
+)
+
+statement ok
+ALTER TABLE test_cast_default RENAME s.a TO c
+
+statement ok
+INSERT INTO test_cast_default DEFAULT VALUES
+
+query I
+SELECT s FROM test_cast_default
+----
+{'c': 1, 'b': 2}