diff --git a/crates/dbx-sql-schema/src/table_structure_sql/column_alter.rs b/crates/dbx-sql-schema/src/table_structure_sql/column_alter.rs index 61124bb80c..89bf52d491 100644 --- a/crates/dbx-sql-schema/src/table_structure_sql/column_alter.rs +++ b/crates/dbx-sql-schema/src/table_structure_sql/column_alter.rs @@ -1020,6 +1020,20 @@ pub(super) fn build_sqlite_existing_column_sql( statements } +pub(super) fn build_duckdb_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec { + let Some(original) = &column.original else { + return Vec::new(); + }; + if column.name == original.name { + return Vec::new(); + } + vec![format!( + "ALTER TABLE {table} RENAME COLUMN {} TO {};", + quote_ident(StructureDialect::DuckDb, &original.name), + quote_ident(StructureDialect::DuckDb, &column.name) + )] +} + pub(super) fn build_questdb_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec { let Some(original) = &column.original else { return Vec::new(); diff --git a/crates/dbx-sql-schema/src/table_structure_sql/columns.rs b/crates/dbx-sql-schema/src/table_structure_sql/columns.rs index d731bcdf03..ebc8b8b89f 100644 --- a/crates/dbx-sql-schema/src/table_structure_sql/columns.rs +++ b/crates/dbx-sql-schema/src/table_structure_sql/columns.rs @@ -1,10 +1,11 @@ use super::column_alter::{ build_clickhouse_existing_column_sql, build_dameng_existing_column_sql, build_doris_existing_column_sql, - build_h2_existing_column_sql, build_informix_existing_column_sql, build_iris_existing_column_sql, - build_mysql_existing_column_clause, build_oracle_like_existing_column_sql, build_oscar_existing_column_sql, - build_postgres_existing_column_sql, build_questdb_existing_column_sql, build_sqlite_existing_column_sql, - build_sqlserver_existing_column_sql, build_xugu_existing_column_sql, dameng_drops_identity, - has_column_extra_change, has_existing_column_attribute_change, validate_dameng_existing_identity_change, + build_duckdb_existing_column_sql, build_h2_existing_column_sql, build_informix_existing_column_sql, + build_iris_existing_column_sql, build_mysql_existing_column_clause, build_oracle_like_existing_column_sql, + build_oscar_existing_column_sql, build_postgres_existing_column_sql, build_questdb_existing_column_sql, + build_sqlite_existing_column_sql, build_sqlserver_existing_column_sql, build_xugu_existing_column_sql, + dameng_drops_identity, has_column_extra_change, has_existing_column_attribute_change, + validate_dameng_existing_identity_change, }; use super::column_format::{ column_definition, has_dameng_identity, is_dameng_identity_compatible_type, is_mysql_character_data_type, @@ -291,6 +292,7 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu warnings, )), StructureDialect::Sqlite => statements.extend(build_sqlite_existing_column_sql(&table, column, warnings)), + StructureDialect::DuckDb => statements.extend(build_duckdb_existing_column_sql(&table, column)), StructureDialect::Questdb => statements.extend(build_questdb_existing_column_sql(&table, column)), _ => warnings.push(format!("Editing existing columns is not supported for {database_label} yet.")), } diff --git a/crates/dbx-sql-schema/src/table_structure_sql/tests.rs b/crates/dbx-sql-schema/src/table_structure_sql/tests.rs index 59d83df205..f29d8c698b 100644 --- a/crates/dbx-sql-schema/src/table_structure_sql/tests.rs +++ b/crates/dbx-sql-schema/src/table_structure_sql/tests.rs @@ -4123,6 +4123,81 @@ fn builds_duckdb_create_table_statements() { ); } +#[cfg(feature = "duckdb-sidecar")] +fn existing_duckdb_column(name: &str) -> EditableStructureColumn { + let mut existing = column(name); + existing.original = Some(ColumnInfo { + name: name.to_string(), + data_type: existing.data_type.clone(), + is_nullable: existing.is_nullable, + column_default: None, + ..Default::default() + }); + existing +} + +#[cfg(feature = "duckdb-sidecar")] +#[test] +fn duckdb_column_rename_generates_quoted_sql() { + let mut renamed = existing_duckdb_column("old_name"); + renamed.name = "new_name".to_string(); + + let result = + build_table_structure_change_sql(structure_change_options(DatabaseType::DuckDb, None, "events", vec![renamed])); + + assert!(result.warnings.is_empty(), "{:?}", result.warnings); + assert_eq!(result.statements, vec![r#"ALTER TABLE "events" RENAME COLUMN "old_name" TO "new_name";"#]); +} + +#[cfg(feature = "duckdb-sidecar")] +#[test] +fn duckdb_column_rename_quotes_special_identifiers() { + let mut renamed = existing_duckdb_column("old \"code\""); + renamed.name = "select value".to_string(); + + let result = build_table_structure_change_sql(structure_change_options( + DatabaseType::DuckDb, + None, + "event log", + vec![renamed], + )); + + assert!(result.warnings.is_empty(), "{:?}", result.warnings); + assert_eq!(result.statements, vec![r#"ALTER TABLE "event log" RENAME COLUMN "old ""code""" TO "select value";"#]); +} + +#[cfg(feature = "duckdb-sidecar")] +#[test] +fn duckdb_column_rename_ignores_unchanged_columns() { + let unchanged = existing_duckdb_column("event_id"); + + let result = build_table_structure_change_sql(structure_change_options( + DatabaseType::DuckDb, + None, + "events", + vec![unchanged], + )); + + assert!(result.statements.is_empty()); + assert!(result.warnings.is_empty()); +} + +#[cfg(feature = "duckdb-sidecar")] +#[test] +fn duckdb_column_rename_with_unsupported_attribute_change_fails_closed() { + let mut changed = existing_duckdb_column("event_id"); + changed.name = "renamed_id".to_string(); + changed.data_type = "BIGINT".to_string(); + changed.is_nullable = false; + changed.default_value = "42".to_string(); + + let result = + build_table_structure_change_sql(structure_change_options(DatabaseType::DuckDb, None, "events", vec![changed])); + + assert!(result.statements.is_empty()); + assert_eq!(result.warnings, vec!["Editing existing columns is not supported for duckdb yet."]); +} + #[test] fn builds_clickhouse_nullable_comment_and_reorder_statements() { let mut source = column("source");