diff --git a/superset/commands/database/uploaders/base.py b/superset/commands/database/uploaders/base.py index 18b4f8024f4..f8e2b389d84 100644 --- a/superset/commands/database/uploaders/base.py +++ b/superset/commands/database/uploaders/base.py @@ -159,6 +159,12 @@ class UploadCommand(BaseCommand): if not self._model: return + self._table_name, self._schema = ( + self._model.db_engine_spec.normalize_table_name_for_upload( + self._table_name, self._schema + ) + ) + self._reader.read(self._file, self._model, self._table_name, self._schema) sqla_table = ( diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index 4113a3e8fe5..396c3805acd 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -1295,6 +1295,26 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods return None + @classmethod + def normalize_table_name_for_upload( + cls, + table_name: str, + schema_name: str | None = None, + ) -> tuple[str, str | None]: + """ + Normalize table and schema names for file upload. + + Some databases (e.g., Redshift) fold unquoted identifiers to lowercase, + which can cause issues when the upload creates a table with one case + but metadata operations use a different case. Override this method + to normalize names according to database-specific rules. + + :param table_name: The table name to normalize + :param schema_name: The schema name to normalize (optional) + :return: Tuple of (normalized_table_name, normalized_schema_name) + """ + return table_name, schema_name + @classmethod def df_to_sql( cls, diff --git a/superset/db_engine_specs/redshift.py b/superset/db_engine_specs/redshift.py index df8ee834fc4..ea49c479dea 100644 --- a/superset/db_engine_specs/redshift.py +++ b/superset/db_engine_specs/redshift.py @@ -183,6 +183,24 @@ class RedshiftEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec): ), } + @classmethod + def normalize_table_name_for_upload( + cls, + table_name: str, + schema_name: str | None = None, + ) -> tuple[str, str | None]: + """ + Redshift folds unquoted identifiers to lowercase. + + :param table_name: The table name to normalize + :param schema_name: The schema name to normalize (optional) + :return: Tuple of (normalized_table_name, normalized_schema_name) + """ + return ( + table_name.lower(), + schema_name.lower() if schema_name else None, + ) + @classmethod def df_to_sql( cls, diff --git a/tests/unit_tests/db_engine_specs/test_redshift.py b/tests/unit_tests/db_engine_specs/test_redshift.py index 77022809075..d733767503f 100644 --- a/tests/unit_tests/db_engine_specs/test_redshift.py +++ b/tests/unit_tests/db_engine_specs/test_redshift.py @@ -20,6 +20,7 @@ from typing import Optional import pytest +from superset.db_engine_specs.redshift import RedshiftEngineSpec from tests.unit_tests.db_engine_specs.utils import assert_convert_dttm from tests.unit_tests.fixtures.common import dttm # noqa: F401 @@ -49,3 +50,32 @@ def test_convert_dttm( ) assert_convert_dttm(spec, target_type, expected_result, dttm) + + +@pytest.mark.parametrize( + "table_name,schema_name,expected_table,expected_schema", + [ + ("BPO_mytest_2", "MySchema", "bpo_mytest_2", "myschema"), + ("MY_TABLE", None, "my_table", None), + ("already_lower", "lower_schema", "already_lower", "lower_schema"), + ], +) +def test_normalize_table_name_for_upload( + table_name: str, + schema_name: Optional[str], + expected_table: str, + expected_schema: Optional[str], +) -> None: + """ + Test that table and schema names are normalized to lowercase for Redshift. + + Redshift folds unquoted identifiers to lowercase, so we need to normalize + table names to ensure consistent behavior when checking table existence + and performing replace operations. + """ + normalized_table, normalized_schema = ( + RedshiftEngineSpec.normalize_table_name_for_upload(table_name, schema_name) + ) + + assert normalized_table == expected_table + assert normalized_schema == expected_schema