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

dpgaspar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 1b375b715c refactor: upload data unification, less permissions and 
less endpoints (#31959)
1b375b715c is described below

commit 1b375b715cf844d0cc4b61428a4e63abe0ec7369
Author: Daniel Vaz Gaspar <[email protected]>
AuthorDate: Tue Jan 28 11:09:55 2025 +0000

    refactor: upload data unification, less permissions and less endpoints 
(#31959)
---
 UPDATING.md                                        |   1 +
 .../UploadDataModel/UploadDataModal.test.tsx       |  33 ++-
 .../features/databases/UploadDataModel/index.tsx   |  21 +-
 .../src/features/home/RightMenu.test.tsx           |   3 +-
 superset-frontend/src/views/CRUD/utils.tsx         |   6 +-
 superset/commands/database/uploaders/base.py       |   7 +
 superset/constants.py                              |   4 +-
 superset/databases/api.py                          | 300 ++++-----------------
 superset/databases/schemas.py                      | 209 ++++----------
 ...-34_74ad1125881c_converge_upload_permissions.py |  84 ++++++
 superset/security/manager.py                       |   3 +-
 tests/integration_tests/databases/api_tests.py     |   4 +-
 tests/integration_tests/security_tests.py          |   4 +-
 tests/unit_tests/databases/api_test.py             | 120 ++++++---
 14 files changed, 311 insertions(+), 488 deletions(-)

diff --git a/UPDATING.md b/UPDATING.md
index 2a9e8992d8..1f86b5a0c0 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -24,6 +24,7 @@ assists people when migrating to a new version.
 
 ## Next
 
+- [31959](https://github.com/apache/superset/pull/31959) Removes the following 
endpoints from data uploads: /api/v1/database/<id>/<file type>_upload and 
/api/v1/database/<file type>_metadata, in favour of new one (Details on the 
PR). And simplifies permissions.
 - [31844](https://github.com/apache/superset/pull/31844) The 
`ALERT_REPORTS_EXECUTE_AS` and `THUMBNAILS_EXECUTE_AS` config parameters have 
been renamed to `ALERT_REPORTS_EXECUTORS` and `THUMBNAILS_EXECUTORS` 
respectively. A new config flag `CACHE_WARMUP_EXECUTORS` has also been 
introduced to be able to control which user is used to execute cache warmup 
tasks. Finally, the config flag `THUMBNAILS_SELENIUM_USER` has been removed. To 
use a fixed executor for async tasks, use the new `FixedE [...]
 - [31894](https://github.com/apache/superset/pull/31894) Domain sharding is 
deprecated in favor of HTTP2. The `SUPERSET_WEBSERVER_DOMAINS` configuration 
will be removed in the next major version (6.0)
 - [31774](https://github.com/apache/superset/pull/31774): Fixes the spelling 
of the `USE-ANALAGOUS-COLORS` feature flag. Please update any 
scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag 
spelling.
diff --git 
a/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
 
b/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
index 7347e13bd5..cb084f6a0d 100644
--- 
a/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
+++ 
b/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
@@ -25,9 +25,7 @@ import userEvent from '@testing-library/user-event';
 import { waitFor } from '@testing-library/react';
 import { UploadFile } from 'antd/lib/upload/interface';
 
-fetchMock.post('glob:*api/v1/database/1/csv_upload/', {});
-fetchMock.post('glob:*api/v1/database/1/excel_upload/', {});
-fetchMock.post('glob:*api/v1/database/1/columnar_upload/', {});
+fetchMock.post('glob:*api/v1/database/1/upload/', {});
 
 fetchMock.get(
   
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:eq,value:!t)),page:0,page_size:100)',
@@ -643,18 +641,21 @@ test('CSV, form post', async () => {
   });
 
   userEvent.click(uploadButton);
-  await waitFor(() => fetchMock.called('glob:*api/v1/database/1/csv_upload/'));
+  await waitFor(() => fetchMock.called('glob:*api/v1/database/1/upload/'));
 
   // Get the matching fetch calls made
-  const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/csv_upload/');
+  const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/upload/');
   expect(matchingCalls).toHaveLength(1);
   const [_, options] = matchingCalls[0];
   const formData = options?.body as FormData;
+  expect(formData.get('type')).toBe('csv');
   expect(formData.get('table_name')).toBe('table1');
   expect(formData.get('schema')).toBe('public');
   expect(formData.get('table_name')).toBe('table1');
   const fileData = formData.get('file') as File;
   expect(fileData.name).toBe('test.csv');
+  // Avoid leaking fetchMock calls
+  fetchMock.resetHistory();
 });
 
 test('Excel, form post', async () => {
@@ -700,22 +701,21 @@ test('Excel, form post', async () => {
   });
 
   userEvent.click(uploadButton);
-  await waitFor(() =>
-    fetchMock.called('glob:*api/v1/database/1/excel_upload/'),
-  );
+  await waitFor(() => fetchMock.called('glob:*api/v1/database/1/upload/'));
 
   // Get the matching fetch calls made
-  const matchingCalls = fetchMock.calls(
-    'glob:*api/v1/database/1/excel_upload/',
-  );
+  const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/upload/');
   expect(matchingCalls).toHaveLength(1);
   const [_, options] = matchingCalls[0];
   const formData = options?.body as FormData;
+  expect(formData.get('type')).toBe('excel');
   expect(formData.get('table_name')).toBe('table1');
   expect(formData.get('schema')).toBe('public');
   expect(formData.get('table_name')).toBe('table1');
   const fileData = formData.get('file') as File;
   expect(fileData.name).toBe('test.xls');
+  // Avoid leaking fetchMock calls
+  fetchMock.resetHistory();
 });
 
 test('Columnar, form post', async () => {
@@ -761,22 +761,21 @@ test('Columnar, form post', async () => {
   });
 
   userEvent.click(uploadButton);
-  await waitFor(() =>
-    fetchMock.called('glob:*api/v1/database/1/columnar_upload/'),
-  );
+  await waitFor(() => fetchMock.called('glob:*api/v1/database/1/upload/'));
 
   // Get the matching fetch calls made
-  const matchingCalls = fetchMock.calls(
-    'glob:*api/v1/database/1/columnar_upload/',
-  );
+  const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/upload/');
   expect(matchingCalls).toHaveLength(1);
   const [_, options] = matchingCalls[0];
   const formData = options?.body as FormData;
+  expect(formData.get('type')).toBe('columnar');
   expect(formData.get('table_name')).toBe('table1');
   expect(formData.get('schema')).toBe('public');
   expect(formData.get('table_name')).toBe('table1');
   const fileData = formData.get('file') as File;
   expect(fileData.name).toBe('test.parquet');
+  // Avoid leaking fetchMock calls
+  fetchMock.resetHistory();
 });
 
 test('CSV, validate file extension returns false', () => {
diff --git a/superset-frontend/src/features/databases/UploadDataModel/index.tsx 
b/superset-frontend/src/features/databases/UploadDataModel/index.tsx
index cc3215a68e..eccfe3da37 100644
--- a/superset-frontend/src/features/databases/UploadDataModel/index.tsx
+++ b/superset-frontend/src/features/databases/UploadDataModel/index.tsx
@@ -230,19 +230,8 @@ const UploadDataModal: 
FunctionComponent<UploadDataModalProps> = ({
   const [previewUploadedFile, setPreviewUploadedFile] = 
useState<boolean>(true);
   const [fileLoading, setFileLoading] = useState<boolean>(false);
 
-  const createTypeToEndpointMap = (
-    databaseId: number,
-  ): { [key: string]: string } => ({
-    csv: `/api/v1/database/${databaseId}/csv_upload/`,
-    excel: `/api/v1/database/${databaseId}/excel_upload/`,
-    columnar: `/api/v1/database/${databaseId}/columnar_upload/`,
-  });
-
-  const typeToFileMetadataEndpoint = {
-    csv: '/api/v1/database/csv_metadata/',
-    excel: '/api/v1/database/excel_metadata/',
-    columnar: '/api/v1/database/columnar_metadata/',
-  };
+  const createTypeToEndpointMap = (databaseId: number) =>
+    `/api/v1/database/${databaseId}/upload/`;
 
   const nullValuesOptions = [
     {
@@ -389,9 +378,10 @@ const UploadDataModal: 
FunctionComponent<UploadDataModalProps> = ({
     if (type === 'csv') {
       formData.append('delimiter', mergedValues.delimiter);
     }
+    formData.append('type', type);
     setFileLoading(true);
     return SupersetClient.post({
-      endpoint: typeToFileMetadataEndpoint[type],
+      endpoint: '/api/v1/database/upload_metadata/',
       body: formData,
       headers: { Accept: 'application/json' },
     })
@@ -472,7 +462,8 @@ const UploadDataModal: 
FunctionComponent<UploadDataModalProps> = ({
     }
     appendFormData(formData, mergedValues);
     setIsLoading(true);
-    const endpoint = createTypeToEndpointMap(currentDatabaseId)[type];
+    const endpoint = createTypeToEndpointMap(currentDatabaseId);
+    formData.append('type', type);
     return SupersetClient.post({
       endpoint,
       body: formData,
diff --git a/superset-frontend/src/features/home/RightMenu.test.tsx 
b/superset-frontend/src/features/home/RightMenu.test.tsx
index fd0d2230af..a7e8322170 100644
--- a/superset-frontend/src/features/home/RightMenu.test.tsx
+++ b/superset-frontend/src/features/home/RightMenu.test.tsx
@@ -168,8 +168,7 @@ const resetUseSelectorMock = () => {
     permissions: {},
     roles: {
       Admin: [
-        ['can_csv_upload', 'Database'], // So we can upload CSV
-        ['can_excel_upload', 'Database'], // So we can upload CSV
+        ['can_upload', 'Database'], // So we can upload data (CSV, Excel, 
Columnar)
         ['can_write', 'Database'], // So we can write DBs
         ['can_write', 'Dataset'], // So we can write Datasets
         ['can_write', 'Chart'], // So we can write Datasets
diff --git a/superset-frontend/src/views/CRUD/utils.tsx 
b/superset-frontend/src/views/CRUD/utils.tsx
index 785ac7f7ac..8fd77373b5 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -507,14 +507,14 @@ export const uploadUserPerms = (
   allowedExt: Array<string>,
 ) => {
   const canUploadCSV =
-    findPermission('can_csv_upload', 'Database', roles) &&
+    findPermission('can_upload', 'Database', roles) &&
     checkUploadExtensions(csvExt, allowedExt);
   const canUploadColumnar =
     checkUploadExtensions(colExt, allowedExt) &&
-    findPermission('can_columnar_upload', 'Database', roles);
+    findPermission('can_upload', 'Database', roles);
   const canUploadExcel =
     checkUploadExtensions(excelExt, allowedExt) &&
-    findPermission('can_excel_upload', 'Database', roles);
+    findPermission('can_upload', 'Database', roles);
   return {
     canUploadCSV,
     canUploadColumnar,
diff --git a/superset/commands/database/uploaders/base.py 
b/superset/commands/database/uploaders/base.py
index 0e939ef432..5a8227b054 100644
--- a/superset/commands/database/uploaders/base.py
+++ b/superset/commands/database/uploaders/base.py
@@ -36,6 +36,7 @@ from superset.connectors.sqla.models import SqlaTable
 from superset.daos.database import DatabaseDAO
 from superset.models.core import Database
 from superset.sql_parse import Table
+from superset.utils.backports import StrEnum
 from superset.utils.core import get_user
 from superset.utils.decorators import on_error, transaction
 from superset.views.database.validators import schema_allows_file_upload
@@ -45,6 +46,12 @@ logger = logging.getLogger(__name__)
 READ_CHUNK_SIZE = 1000
 
 
+class UploadFileType(StrEnum):
+    CSV = "csv"
+    EXCEL = "excel"
+    COLUMNAR = "columnar"
+
+
 class ReaderOptions(TypedDict, total=False):
     already_exists: str
     index_label: str
diff --git a/superset/constants.py b/superset/constants.py
index b805406311..3374b2bd90 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -169,9 +169,7 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = {
     "delete_object": "write",
     "copy_dash": "write",
     "get_connection": "write",
-    "excel_metadata": "excel_upload",
-    "columnar_metadata": "columnar_upload",
-    "csv_metadata": "csv_upload",
+    "upload_metadata": "upload",
     "slack_channels": "write",
     "put_filters": "write",
     "put_colors": "write",
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 5c97ac688b..f9c1dfd122 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -55,7 +55,11 @@ from superset.commands.database.ssh_tunnel.exceptions import 
(
 from superset.commands.database.tables import TablesDatabaseCommand
 from superset.commands.database.test_connection import 
TestConnectionDatabaseCommand
 from superset.commands.database.update import UpdateDatabaseCommand
-from superset.commands.database.uploaders.base import UploadCommand
+from superset.commands.database.uploaders.base import (
+    BaseDataReader,
+    UploadCommand,
+    UploadFileType,
+)
 from superset.commands.database.uploaders.columnar_reader import ColumnarReader
 from superset.commands.database.uploaders.csv_reader import CSVReader
 from superset.commands.database.uploaders.excel_reader import ExcelReader
@@ -72,10 +76,6 @@ from superset.databases.decorators import check_table_access
 from superset.databases.filters import DatabaseFilter, 
DatabaseUploadEnabledFilter
 from superset.databases.schemas import (
     CatalogsResponseSchema,
-    ColumnarMetadataUploadFilePostSchema,
-    ColumnarUploadPostSchema,
-    CSVMetadataUploadFilePostSchema,
-    CSVUploadPostSchema,
     database_catalogs_query_schema,
     database_schemas_query_schema,
     database_tables_query_schema,
@@ -88,8 +88,6 @@ from superset.databases.schemas import (
     DatabaseTablesResponse,
     DatabaseTestConnectionSchema,
     DatabaseValidateParametersSchema,
-    ExcelMetadataUploadFilePostSchema,
-    ExcelUploadPostSchema,
     get_export_ids_schema,
     OAuth2ProviderResponseSchema,
     openapi_spec_methods_override,
@@ -99,6 +97,8 @@ from superset.databases.schemas import (
     TableExtraMetadataResponseSchema,
     TableMetadataResponseSchema,
     UploadFileMetadata,
+    UploadFileMetadataPostSchema,
+    UploadPostSchema,
     ValidateSQLRequest,
     ValidateSQLResponse,
 )
@@ -162,12 +162,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         "delete_ssh_tunnel",
         "schemas_access_for_file_upload",
         "get_connection",
-        "csv_upload",
-        "csv_metadata",
-        "excel_upload",
-        "excel_metadata",
-        "columnar_upload",
-        "columnar_metadata",
+        "upload_metadata",
+        "upload",
         "oauth2",
     }
 
@@ -282,8 +278,6 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
     openapi_spec_tag = "Database"
     openapi_spec_component_schemas = (
         CatalogsResponseSchema,
-        ColumnarUploadPostSchema,
-        CSVUploadPostSchema,
         DatabaseConnectionSchema,
         DatabaseFunctionNamesResponse,
         DatabaseSchemaAccessForFileUploadResponse,
@@ -291,15 +285,13 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         DatabaseTablesResponse,
         DatabaseTestConnectionSchema,
         DatabaseValidateParametersSchema,
-        ExcelUploadPostSchema,
         TableExtraMetadataResponseSchema,
         TableMetadataResponseSchema,
         SelectStarResponseSchema,
         SchemasResponseSchema,
-        CSVMetadataUploadFilePostSchema,
-        ExcelMetadataUploadFilePostSchema,
-        ColumnarMetadataUploadFilePostSchema,
+        UploadFileMetadataPostSchema,
         UploadFileMetadata,
+        UploadPostSchema,
         ValidateSQLRequest,
         ValidateSQLResponse,
     )
@@ -1621,30 +1613,31 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         command.run()
         return self.response(200, message="OK")
 
-    @expose("/csv_metadata/", methods=("POST",))
+    @expose("/upload_metadata/", methods=("POST",))
     @protect()
     @statsd_metrics
     @event_logger.log_this_with_context(
         action=(
-            lambda self, *args, **kwargs: f"{self.__class__.__name__}" 
".csv_metadata"
+            lambda self, *args, **kwargs: f"{self.__class__.__name__}"
+            ".upload_metadata"
         ),
         log_to_statsd=False,
     )
     @requires_form_data
-    def csv_metadata(self) -> Response:
-        """Upload a CSV file and returns file metadata.
+    def upload_metadata(self) -> Response:
+        """Upload a file and returns file metadata.
         ---
         post:
-          summary: Upload a CSV file and returns file metadata
+          summary: Upload a file and returns file metadata
           requestBody:
             required: true
             content:
               multipart/form-data:
                 schema:
-                  $ref: '#/components/schemas/CSVMetadataUploadFilePostSchema'
+                  $ref: '#/components/schemas/UploadFileMetadataPostSchema'
           responses:
             200:
-              description: Columnar upload response
+              description: Upload response
               content:
                 application/json:
                   schema:
@@ -1664,25 +1657,32 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         try:
             request_form = request.form.to_dict()
             request_form["file"] = request.files.get("file")
-            parameters = CSVMetadataUploadFilePostSchema().load(request_form)
+            parameters = UploadFileMetadataPostSchema().load(request_form)
         except ValidationError as error:
             return self.response_400(message=error.messages)
-        metadata = CSVReader(parameters).file_metadata(parameters["file"])
+        if parameters["type"] == UploadFileType.CSV.value:
+            metadata = CSVReader(parameters).file_metadata(parameters["file"])
+        elif parameters["type"] == UploadFileType.EXCEL.value:
+            metadata = 
ExcelReader(parameters).file_metadata(parameters["file"])
+        elif parameters["type"] == UploadFileType.COLUMNAR.value:
+            metadata = 
ColumnarReader(parameters).file_metadata(parameters["file"])
+        else:
+            self.response_400(message="Unexpected Invalid file type")
         return self.response(200, result=UploadFileMetadata().dump(metadata))
 
-    @expose("/<int:pk>/csv_upload/", methods=("POST",))
+    @expose("/<int:pk>/upload/", methods=("POST",))
     @protect()
     @statsd_metrics
     @event_logger.log_this_with_context(
-        action=lambda self, *args, **kwargs: 
f"{self.__class__.__name__}.csv_upload",
+        action=lambda self, *args, **kwargs: 
f"{self.__class__.__name__}.upload",
         log_to_statsd=False,
     )
     @requires_form_data
-    def csv_upload(self, pk: int) -> Response:
-        """Upload a CSV file into a database.
+    def upload(self, pk: int) -> Response:
+        """Upload a file into a database.
         ---
         post:
-          summary: Upload a CSV file to a database table
+          summary: Upload a file to a database table
           parameters:
           - in: path
             schema:
@@ -1693,7 +1693,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
             content:
               multipart/form-data:
                 schema:
-                  $ref: '#/components/schemas/CSVUploadPostSchema'
+                  $ref: '#/components/schemas/UploadPostSchema'
           responses:
             201:
               description: CSV upload response
@@ -1718,232 +1718,22 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         try:
             request_form = request.form.to_dict()
             request_form["file"] = request.files.get("file")
-            parameters = CSVUploadPostSchema().load(request_form)
-            UploadCommand(
-                pk,
-                parameters["table_name"],
-                parameters["file"],
-                parameters.get("schema"),
-                CSVReader(parameters),
-            ).run()
-        except ValidationError as error:
-            return self.response_400(message=error.messages)
-        return self.response(201, message="OK")
-
-    @expose("/excel_metadata/", methods=("POST",))
-    @protect()
-    @statsd_metrics
-    @event_logger.log_this_with_context(
-        action=(
-            lambda self, *args, **kwargs: f"{self.__class__.__name__}" 
".excel_metadata"
-        ),
-        log_to_statsd=False,
-    )
-    @requires_form_data
-    def excel_metadata(self) -> Response:
-        """Upload an Excel file and returns file metadata.
-        ---
-        post:
-          summary: Upload an Excel file and returns file metadata
-          requestBody:
-            required: true
-            content:
-              multipart/form-data:
-                schema:
-                  $ref: 
'#/components/schemas/ExcelMetadataUploadFilePostSchema'
-          responses:
-            200:
-              description: Columnar upload response
-              content:
-                application/json:
-                  schema:
-                    type: object
-                    properties:
-                      result:
-                        $ref: '#/components/schemas/UploadFileMetadata'
-            400:
-              $ref: '#/components/responses/400'
-            401:
-              $ref: '#/components/responses/401'
-            404:
-              $ref: '#/components/responses/404'
-            500:
-              $ref: '#/components/responses/500'
-        """
-        try:
-            request_form = request.form.to_dict()
-            request_form["file"] = request.files.get("file")
-            parameters = ExcelMetadataUploadFilePostSchema().load(request_form)
-        except ValidationError as error:
-            return self.response_400(message=error.messages)
-        metadata = ExcelReader().file_metadata(parameters["file"])
-        return self.response(200, result=UploadFileMetadata().dump(metadata))
-
-    @expose("/<int:pk>/excel_upload/", methods=("POST",))
-    @protect()
-    @statsd_metrics
-    @event_logger.log_this_with_context(
-        action=lambda self, *args, **kwargs: 
f"{self.__class__.__name__}.excel_upload",
-        log_to_statsd=False,
-    )
-    @requires_form_data
-    def excel_upload(self, pk: int) -> Response:
-        """Upload an Excel file into a database.
-        ---
-        post:
-          summary: Upload an Excel file to a database table
-          parameters:
-          - in: path
-            schema:
-              type: integer
-            name: pk
-          requestBody:
-            required: true
-            content:
-              multipart/form-data:
-                schema:
-                  $ref: '#/components/schemas/ExcelUploadPostSchema'
-          responses:
-            201:
-              description: Excel upload response
-              content:
-                application/json:
-                  schema:
-                    type: object
-                    properties:
-                      message:
-                        type: string
-            400:
-              $ref: '#/components/responses/400'
-            401:
-              $ref: '#/components/responses/401'
-            404:
-              $ref: '#/components/responses/404'
-            422:
-              $ref: '#/components/responses/422'
-            500:
-              $ref: '#/components/responses/500'
-        """
-        try:
-            request_form = request.form.to_dict()
-            request_form["file"] = request.files.get("file")
-            parameters = ExcelUploadPostSchema().load(request_form)
-            UploadCommand(
-                pk,
-                parameters["table_name"],
-                parameters["file"],
-                parameters.get("schema"),
-                ExcelReader(parameters),
-            ).run()
-        except ValidationError as error:
-            return self.response_400(message=error.messages)
-        return self.response(201, message="OK")
-
-    @expose("/columnar_metadata/", methods=("POST",))
-    @protect()
-    @statsd_metrics
-    @event_logger.log_this_with_context(
-        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
-        ".columnar_metadata",
-        log_to_statsd=False,
-    )
-    @requires_form_data
-    def columnar_metadata(self) -> Response:
-        """Upload a Columnar file and returns file metadata.
-        ---
-        post:
-          summary: Upload a Columnar file and returns file metadata
-          requestBody:
-            required: true
-            content:
-              multipart/form-data:
-                schema:
-                  $ref: 
'#/components/schemas/ColumnarMetadataUploadFilePostSchema'
-          responses:
-            200:
-              description: Columnar upload response
-              content:
-                application/json:
-                  schema:
-                    type: object
-                    properties:
-                      result:
-                        $ref: '#/components/schemas/UploadFileMetadata'
-            400:
-              $ref: '#/components/responses/400'
-            401:
-              $ref: '#/components/responses/401'
-            404:
-              $ref: '#/components/responses/404'
-            500:
-              $ref: '#/components/responses/500'
-        """
-        try:
-            request_form = request.form.to_dict()
-            request_form["file"] = request.files.get("file")
-            parameters = 
ColumnarMetadataUploadFilePostSchema().load(request_form)
-        except ValidationError as error:
-            return self.response_400(message=error.messages)
-        metadata = ColumnarReader().file_metadata(parameters["file"])
-        return self.response(200, result=UploadFileMetadata().dump(metadata))
-
-    @expose("/<int:pk>/columnar_upload/", methods=("POST",))
-    @protect()
-    @statsd_metrics
-    @event_logger.log_this_with_context(
-        action=lambda self,
-        *args,
-        **kwargs: f"{self.__class__.__name__}.columnar_upload",
-        log_to_statsd=False,
-    )
-    @requires_form_data
-    def columnar_upload(self, pk: int) -> Response:
-        """Upload a Columnar file into a database.
-        ---
-        post:
-          summary: Upload a Columnar file to a database table
-          parameters:
-          - in: path
-            schema:
-              type: integer
-            name: pk
-          requestBody:
-            required: true
-            content:
-              multipart/form-data:
-                schema:
-                  $ref: '#/components/schemas/ColumnarUploadPostSchema'
-          responses:
-            201:
-              description: Columnar upload response
-              content:
-                application/json:
-                  schema:
-                    type: object
-                    properties:
-                      message:
-                        type: string
-            400:
-              $ref: '#/components/responses/400'
-            401:
-              $ref: '#/components/responses/401'
-            404:
-              $ref: '#/components/responses/404'
-            422:
-              $ref: '#/components/responses/422'
-            500:
-              $ref: '#/components/responses/500'
-        """
-        try:
-            request_form = request.form.to_dict()
-            request_form["file"] = request.files.get("file")
-            parameters = ColumnarUploadPostSchema().load(request_form)
+            parameters = UploadPostSchema().load(request_form)
+            reader: BaseDataReader
+            if parameters["type"] == UploadFileType.CSV.value:
+                reader = CSVReader(parameters)
+            elif parameters["type"] == UploadFileType.EXCEL.value:
+                reader = ExcelReader(parameters)
+            elif parameters["type"] == UploadFileType.COLUMNAR.value:
+                reader = ColumnarReader(parameters)
+            else:
+                return self.response_400(message="Unexpected Invalid file 
type")
             UploadCommand(
                 pk,
                 parameters["table_name"],
                 parameters["file"],
                 parameters.get("schema"),
-                ColumnarReader(parameters),
+                reader,
             ).run()
         except ValidationError as error:
             return self.response_400(message=error.messages)
diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py
index 66366d9519..8312a7a1b2 100644
--- a/superset/databases/schemas.py
+++ b/superset/databases/schemas.py
@@ -45,6 +45,7 @@ from superset.commands.database.ssh_tunnel.exceptions import (
     SSHTunnelInvalidCredentials,
     SSHTunnelMissingCredentials,
 )
+from superset.commands.database.uploaders.base import UploadFileType
 from superset.constants import PASSWORD_MASK
 from superset.databases.types import (  # pylint:disable=unused-import
     EncryptedDict,  # noqa: F401
@@ -1081,20 +1082,22 @@ class DelimitedListField(fields.List):
             ) from exc
 
 
-class BaseUploadFilePostSchema(Schema):
-    _extension_config_key = ""
-
+class BaseUploadFilePostSchemaMixin(Schema):
     @validates("file")
     def validate_file_extension(self, file: FileStorage) -> None:
-        allowed_extensions = 
current_app.config["ALLOWED_EXTENSIONS"].intersection(
-            current_app.config[self._extension_config_key]
-        )
+        allowed_extensions = current_app.config["ALLOWED_EXTENSIONS"]
         file_suffix = Path(file.filename).suffix
         if not file_suffix or file_suffix[1:] not in allowed_extensions:
             raise ValidationError([_("File extension is not allowed.")])
 
 
-class BaseUploadPostSchema(BaseUploadFilePostSchema):
+class UploadPostSchema(BaseUploadFilePostSchemaMixin):
+    type = fields.Enum(
+        UploadFileType,
+        required=True,
+        by_value=True,
+        metadata={"description": "File type to upload"},
+    )
     already_exists = fields.String(
         load_default="fail",
         validate=OneOf(choices=("fail", "replace", "append")),
@@ -1123,43 +1126,26 @@ class BaseUploadPostSchema(BaseUploadFilePostSchema):
         metadata={"description": "The name of the table to be 
created/appended"},
     )
 
-
-class ColumnarUploadPostSchema(BaseUploadPostSchema):
-    """
-    Schema for Columnar Upload
-    """
-
-    _extension_config_key = "COLUMNAR_EXTENSIONS"
-
+    # ------------
+    # CSV Schema
+    # ------------
     file = fields.Raw(
         required=True,
         metadata={
-            "description": "The Columnar file to upload",
+            "description": "The file to upload",
             "type": "string",
-            "format": "binary",
+            "format": "text/csv",
         },
     )
-
-
-class CSVUploadPostSchema(BaseUploadPostSchema):
-    """
-    Schema for CSV Upload
-    """
-
-    _extension_config_key = "CSV_EXTENSIONS"
-
-    file = fields.Raw(
-        required=True,
+    delimiter = fields.String(
         metadata={
-            "description": "The CSV file to upload",
-            "type": "string",
-            "format": "text/csv",
-        },
+            "description": "[CSV only] The character used to separate values 
in the CSV"
+            " file (e.g., a comma, semicolon, or tab)."
+        }
     )
-    delimiter = fields.String(metadata={"description": "The delimiter of the 
CSV file"})
     column_data_types = fields.String(
         metadata={
-            "description": "A dictionary with column names and "
+            "description": "[CSV only] A dictionary with column names and "
             "their data types if you need to change "
             "the defaults. Example: {'user_id':'int'}. "
             "Check Python Pandas library for supported data types"
@@ -1167,57 +1153,69 @@ class CSVUploadPostSchema(BaseUploadPostSchema):
     )
     day_first = fields.Boolean(
         metadata={
-            "description": "DD/MM format dates, international and European 
format"
+            "description": "[CSV only] DD/MM format dates, international and 
European"
+            " format"
         }
     )
     skip_blank_lines = fields.Boolean(
-        metadata={"description": "Skip blank lines in the CSV file."}
+        metadata={"description": "[CSV only] Skip blank lines in the CSV 
file."}
     )
     skip_initial_space = fields.Boolean(
-        metadata={"description": "Skip spaces after delimiter."}
+        metadata={"description": "[CSV only] Skip spaces after delimiter."}
     )
     column_dates = DelimitedListField(
         fields.String(),
         metadata={
-            "description": "A list of column names that should be "
+            "description": "[CSV and Excel only] A list of column names that 
should be "
             "parsed as dates. Example: date,timestamp"
         },
     )
     decimal_character = fields.String(
         metadata={
-            "description": "Character to recognize as decimal point. Default 
is '.'"
+            "description": "[CSV and Excel only] Character to recognize as 
decimal"
+            " point. Default is '.'"
         }
     )
     header_row = fields.Integer(
         metadata={
-            "description": "Row containing the headers to use as column names"
-            "(0 is first line of data). Leave empty if there is no header row."
+            "description": "[CSV and Excel only] Row containing the headers to 
use as"
+            " column names (0 is first line of data). Leave empty if"
+            " there is no header row."
         }
     )
     index_column = fields.String(
         metadata={
-            "description": "Column to use as the row labels of the dataframe. "
-            "Leave empty if no index column"
+            "description": "[CSV and Excel only] Column to use as the row 
labels of the"
+            " dataframe. Leave empty if no index column"
         }
     )
     null_values = DelimitedListField(
         fields.String(),
         metadata={
-            "description": "A list of strings that should be treated as null. "
-            "Examples: '' for empty strings, 'None', 'N/A',"
-            "Warning: Hive database supports only a single value"
+            "description": "[CSV and Excel only] A list of strings that should 
be "
+            "treated as null. Examples: '' for empty strings, 'None',"
+            " 'N/A', Warning: Hive database supports only a single value"
         },
     )
     rows_to_read = fields.Integer(
         metadata={
-            "description": "Number of rows to read from the file. "
+            "description": "[CSV and Excel only] Number of rows to read from 
the file. "
             "If None, reads all rows."
         },
         allow_none=True,
         validate=Range(min=1),
     )
     skip_rows = fields.Integer(
-        metadata={"description": "Number of rows to skip at start of file."}
+        metadata={
+            "description": "[CSV and Excel only] Number of rows to skip at 
start"
+            " of file."
+        }
+    )
+    sheet_name = fields.String(
+        metadata={
+            "description": "[Excel only]] Strings used for sheet names "
+            "(default is the first sheet)."
+        }
     )
 
     @post_load
@@ -1234,79 +1232,17 @@ class CSVUploadPostSchema(BaseUploadPostSchema):
         return data
 
 
-class ExcelUploadPostSchema(BaseUploadPostSchema):
+class UploadFileMetadataPostSchema(BaseUploadFilePostSchemaMixin):
     """
-    Schema for Excel Upload
+    Schema for Upload file metadata.
     """
 
-    _extension_config_key = "EXCEL_EXTENSIONS"
-
-    file = fields.Raw(
+    type = fields.Enum(
+        UploadFileType,
         required=True,
-        metadata={
-            "description": "The Excel file to upload",
-            "type": "string",
-            "format": "binary",
-        },
-    )
-    sheet_name = fields.String(
-        metadata={
-            "description": "Strings used for sheet names "
-            "(default is the first sheet)."
-        }
-    )
-    column_dates = DelimitedListField(
-        fields.String(),
-        metadata={
-            "description": "A list of column names that should be "
-            "parsed as dates. Example: date,timestamp"
-        },
-    )
-    decimal_character = fields.String(
-        metadata={
-            "description": "Character to recognize as decimal point. Default 
is '.'"
-        }
-    )
-    header_row = fields.Integer(
-        metadata={
-            "description": "Row containing the headers to use as column names"
-            "(0 is first line of data). Leave empty if there is no header row."
-        }
-    )
-    index_column = fields.String(
-        metadata={
-            "description": "Column to use as the row labels of the dataframe. "
-            "Leave empty if no index column"
-        }
-    )
-    null_values = DelimitedListField(
-        fields.String(),
-        metadata={
-            "description": "A list of strings that should be treated as null. "
-            "Examples: '' for empty strings, 'None', 'N/A',"
-            "Warning: Hive database supports only a single value"
-        },
-    )
-    rows_to_read = fields.Integer(
-        metadata={
-            "description": "Number of rows to read from the file. "
-            "If None, reads all rows."
-        },
-        allow_none=True,
-        validate=Range(min=1),
-    )
-    skip_rows = fields.Integer(
-        metadata={"description": "Number of rows to skip at start of file."}
+        by_value=True,
+        metadata={"description": "File type to upload"},
     )
-
-
-class CSVMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
-    """
-    Schema for CSV metadata.
-    """
-
-    _extension_config_key = "CSV_EXTENSIONS"
-
     file = fields.Raw(
         required=True,
         metadata={
@@ -1315,30 +1251,12 @@ class 
CSVMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
             "format": "binary",
         },
     )
-    delimiter = fields.String(metadata={"description": "The delimiter of the 
CSV file"})
-    header_row = fields.Integer(
+    delimiter = fields.String(
         metadata={
-            "description": "Row containing the headers to use as column names"
-            "(0 is first line of data). Leave empty if there is no header row."
+            "description": "The character used to separate values in the CSV 
file"
+            " (e.g., a comma, semicolon, or tab)."
         }
     )
-
-
-class ExcelMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
-    """
-    Schema for CSV metadata.
-    """
-
-    _extension_config_key = "EXCEL_EXTENSIONS"
-
-    file = fields.Raw(
-        required=True,
-        metadata={
-            "description": "The file to upload",
-            "type": "string",
-            "format": "binary",
-        },
-    )
     header_row = fields.Integer(
         metadata={
             "description": "Row containing the headers to use as column names"
@@ -1347,23 +1265,6 @@ class 
ExcelMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
     )
 
 
-class ColumnarMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
-    """
-    Schema for CSV metadata.
-    """
-
-    _extension_config_key = "COLUMNAR_EXTENSIONS"
-
-    file = fields.Raw(
-        required=True,
-        metadata={
-            "description": "The file to upload",
-            "type": "string",
-            "format": "binary",
-        },
-    )
-
-
 class UploadFileMetadataItemSchema(Schema):
     sheet_name = fields.String(metadata={"description": "The name of the 
sheet"})
     column_names = fields.List(
diff --git 
a/superset/migrations/versions/2025-01-22_14-34_74ad1125881c_converge_upload_permissions.py
 
b/superset/migrations/versions/2025-01-22_14-34_74ad1125881c_converge_upload_permissions.py
new file mode 100644
index 0000000000..75a5e0ad97
--- /dev/null
+++ 
b/superset/migrations/versions/2025-01-22_14-34_74ad1125881c_converge_upload_permissions.py
@@ -0,0 +1,84 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""converge_upload_permissions
+
+Revision ID: 74ad1125881c
+Revises: d482d51c15ca
+Create Date: 2025-01-22 14:34:25.610084
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "74ad1125881c"
+down_revision = "d482d51c15ca"
+
+from alembic import op  # noqa: E402
+from sqlalchemy.exc import SQLAlchemyError  # noqa: E402
+from sqlalchemy.orm import Session  # noqa: E402
+
+from superset.migrations.shared.security_converge import (  # noqa: E402
+    add_pvms,
+    get_reversed_new_pvms,
+    get_reversed_pvm_map,
+    migrate_roles,
+    Pvm,
+)
+
+NEW_PVMS = {"Database": ("can_upload",)}
+
+PVM_MAP = {
+    Pvm("Database", "can_csv_upload"): (Pvm("Database", "can_upload"),),
+    Pvm("Database", "can_excel_upload"): (Pvm("Database", "can_upload"),),
+    Pvm("Database", "can_columnar_upload"): (Pvm("Database", "can_upload"),),
+}
+
+
+def do_upgrade(session: Session) -> None:
+    add_pvms(session, NEW_PVMS)
+    migrate_roles(session, PVM_MAP)
+
+
+def do_downgrade(session: Session) -> None:
+    add_pvms(session, get_reversed_new_pvms(PVM_MAP))
+    migrate_roles(session, get_reversed_pvm_map(PVM_MAP))
+
+
+def upgrade():
+    bind = op.get_bind()
+    session = Session(bind=bind)
+
+    do_upgrade(session)
+
+    try:
+        session.commit()
+    except SQLAlchemyError as ex:
+        session.rollback()
+        raise Exception(f"An error occurred while upgrading permissions: 
{ex}") from ex
+
+
+def downgrade():
+    bind = op.get_bind()
+    session = Session(bind=bind)
+
+    do_downgrade(session)
+
+    try:
+        session.commit()
+    except SQLAlchemyError as ex:
+        print(f"An error occurred while downgrading permissions: {ex}")
+        session.rollback()
+    pass
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 38fff7a3f4..33cbc814d4 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -270,8 +270,7 @@ class SupersetSecurityManager(  # pylint: 
disable=too-many-public-methods
     }
 
     ALPHA_ONLY_PMVS = {
-        ("can_csv_upload", "Database"),
-        ("can_excel_upload", "Database"),
+        ("can_upload", "Database"),
     }
 
     ADMIN_ONLY_PERMISSIONS = {
diff --git a/tests/integration_tests/databases/api_tests.py 
b/tests/integration_tests/databases/api_tests.py
index 66dfc53401..6c34942f61 100644
--- a/tests/integration_tests/databases/api_tests.py
+++ b/tests/integration_tests/databases/api_tests.py
@@ -1433,11 +1433,9 @@ class TestDatabaseApi(SupersetTestCase):
         assert rv.status_code == 200
         assert set(data["permissions"]) == {
             "can_read",
-            "can_columnar_upload",
-            "can_csv_upload",
-            "can_excel_upload",
             "can_write",
             "can_export",
+            "can_upload",
         }
 
     def test_get_invalid_database_table_metadata(self):
diff --git a/tests/integration_tests/security_tests.py 
b/tests/integration_tests/security_tests.py
index 8be13183ea..f148f9418e 100644
--- a/tests/integration_tests/security_tests.py
+++ b/tests/integration_tests/security_tests.py
@@ -1336,7 +1336,7 @@ class TestRolePermission(SupersetTestCase):
         self.assert_cannot_menu("Upload a CSV", perm_set)
         self.assert_cannot_menu("ReportSchedule", perm_set)
         self.assert_cannot_menu("Alerts & Report", perm_set)
-        assert ("can_csv_upload", "Database") not in perm_set
+        assert ("can_upload", "Database") not in perm_set
 
     def assert_can_gamma(self, perm_set):
         self.assert_can_read("Dataset", perm_set)
@@ -1365,7 +1365,7 @@ class TestRolePermission(SupersetTestCase):
         self.assert_can_all("CssTemplate", perm_set)
         self.assert_can_all("Dataset", perm_set)
         self.assert_can_read("Database", perm_set)
-        assert ("can_csv_upload", "Database") in perm_set
+        assert ("can_upload", "Database") in perm_set
         self.assert_can_menu("Manage", perm_set)
         self.assert_can_menu("Annotation Layers", perm_set)
         self.assert_can_menu("CSS Templates", perm_set)
diff --git a/tests/unit_tests/databases/api_test.py 
b/tests/unit_tests/databases/api_test.py
index be835cb877..63d61ce82a 100644
--- a/tests/unit_tests/databases/api_test.py
+++ b/tests/unit_tests/databases/api_test.py
@@ -842,6 +842,7 @@ def test_oauth2_error(
     [
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -855,6 +856,7 @@ def test_oauth2_error(
             ),
             (
                 {
+                    "type": "csv",
                     "already_exists": "fail",
                     "delimiter": ",",
                     "file": ANY,
@@ -864,6 +866,7 @@ def test_oauth2_error(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table2",
                 "delimiter": ";",
@@ -879,6 +882,7 @@ def test_oauth2_error(
             ),
             (
                 {
+                    "type": "csv",
                     "already_exists": "replace",
                     "column_dates": ["col1", "col2"],
                     "delimiter": ";",
@@ -889,6 +893,7 @@ def test_oauth2_error(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table2",
                 "delimiter": ";",
@@ -911,6 +916,7 @@ def test_oauth2_error(
             ),
             (
                 {
+                    "type": "csv",
                     "already_exists": "replace",
                     "columns_read": ["col1", "col2"],
                     "null_values": ["None", "N/A", "''"],
@@ -945,7 +951,7 @@ def test_csv_upload(
     reader_mock = mocker.patch.object(CSVReader, "__init__")
     reader_mock.return_value = None
     response = client.post(
-        "/api/v1/database/1/csv_upload/",
+        "/api/v1/database/1/upload/",
         data=payload,
         content_type="multipart/form-data",
     )
@@ -960,6 +966,7 @@ def test_csv_upload(
     [
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "delimiter": ",",
                 "already_exists": "fail",
@@ -968,6 +975,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "",
                 "delimiter": ",",
@@ -976,11 +984,17 @@ def test_csv_upload(
             {"message": {"table_name": ["Length must be between 1 and 
10000."]}},
         ),
         (
-            {"table_name": "table1", "delimiter": ",", "already_exists": 
"fail"},
+            {
+                "type": "csv",
+                "table_name": "table1",
+                "delimiter": ",",
+                "already_exists": "fail",
+            },
             {"message": {"file": ["Field may not be null."]}},
         ),
         (
             {
+                "type": "csv",
                 "file": "xpto",
                 "table_name": "table1",
                 "delimiter": ",",
@@ -990,6 +1004,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -999,6 +1014,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -1009,6 +1025,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -1019,6 +1036,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -1029,6 +1047,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -1039,6 +1058,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -1049,6 +1069,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -1059,6 +1080,7 @@ def test_csv_upload(
         ),
         (
             {
+                "type": "csv",
                 "file": (create_csv_file(), "out.csv"),
                 "table_name": "table1",
                 "delimiter": ",",
@@ -1082,7 +1104,7 @@ def test_csv_upload_validation(
     _ = mocker.patch.object(UploadCommand, "run")
 
     response = client.post(
-        "/api/v1/database/1/csv_upload/",
+        "/api/v1/database/1/upload/",
         data=payload,
         content_type="multipart/form-data",
     )
@@ -1116,8 +1138,9 @@ def test_csv_upload_file_extension_invalid(
     """
     _ = mocker.patch.object(UploadCommand, "run")
     response = client.post(
-        "/api/v1/database/1/csv_upload/",
+        "/api/v1/database/1/upload/",
         data={
+            "type": "csv",
             "file": create_csv_file(filename=filename),
             "table_name": "table1",
             "delimiter": ",",
@@ -1152,8 +1175,9 @@ def test_csv_upload_file_extension_valid(
     """
     _ = mocker.patch.object(UploadCommand, "run")
     response = client.post(
-        "/api/v1/database/1/csv_upload/",
+        "/api/v1/database/1/upload/",
         data={
+            "type": "csv",
             "file": create_csv_file(filename=filename),
             "table_name": "table1",
             "delimiter": ",",
@@ -1168,6 +1192,7 @@ def test_csv_upload_file_extension_valid(
     [
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "table1",
             },
@@ -1180,6 +1205,7 @@ def test_csv_upload_file_extension_valid(
             ),
             (
                 {
+                    "type": "excel",
                     "already_exists": "fail",
                     "file": ANY,
                     "table_name": "table1",
@@ -1188,6 +1214,7 @@ def test_csv_upload_file_extension_valid(
         ),
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "table2",
                 "sheet_name": "Sheet1",
@@ -1203,6 +1230,7 @@ def test_csv_upload_file_extension_valid(
             ),
             (
                 {
+                    "type": "excel",
                     "already_exists": "replace",
                     "column_dates": ["col1", "col2"],
                     "sheet_name": "Sheet1",
@@ -1213,6 +1241,7 @@ def test_csv_upload_file_extension_valid(
         ),
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "table2",
                 "sheet_name": "Sheet1",
@@ -1231,6 +1260,7 @@ def test_csv_upload_file_extension_valid(
             ),
             (
                 {
+                    "type": "excel",
                     "already_exists": "replace",
                     "columns_read": ["col1", "col2"],
                     "null_values": ["None", "N/A", "''"],
@@ -1261,7 +1291,7 @@ def test_excel_upload(
     reader_mock = mocker.patch.object(ExcelReader, "__init__")
     reader_mock.return_value = None
     response = client.post(
-        "/api/v1/database/1/excel_upload/",
+        "/api/v1/database/1/upload/",
         data=payload,
         content_type="multipart/form-data",
     )
@@ -1276,6 +1306,7 @@ def test_excel_upload(
     [
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "sheet_name": "Sheet1",
                 "already_exists": "fail",
@@ -1284,6 +1315,7 @@ def test_excel_upload(
         ),
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "",
                 "sheet_name": "Sheet1",
@@ -1292,11 +1324,12 @@ def test_excel_upload(
             {"message": {"table_name": ["Length must be between 1 and 
10000."]}},
         ),
         (
-            {"table_name": "table1", "already_exists": "fail"},
+            {"type": "excel", "table_name": "table1", "already_exists": 
"fail"},
             {"message": {"file": ["Field may not be null."]}},
         ),
         (
             {
+                "type": "excel",
                 "file": "xpto",
                 "table_name": "table1",
                 "already_exists": "fail",
@@ -1305,6 +1338,7 @@ def test_excel_upload(
         ),
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "table1",
                 "already_exists": "xpto",
@@ -1313,6 +1347,7 @@ def test_excel_upload(
         ),
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "table1",
                 "already_exists": "fail",
@@ -1322,6 +1357,7 @@ def test_excel_upload(
         ),
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "table1",
                 "already_exists": "fail",
@@ -1331,6 +1367,7 @@ def test_excel_upload(
         ),
         (
             {
+                "type": "excel",
                 "file": (create_excel_file(), "out.xls"),
                 "table_name": "table1",
                 "already_exists": "fail",
@@ -1353,7 +1390,7 @@ def test_excel_upload_validation(
     _ = mocker.patch.object(UploadCommand, "run")
 
     response = client.post(
-        "/api/v1/database/1/excel_upload/",
+        "/api/v1/database/1/upload/",
         data=payload,
         content_type="multipart/form-data",
     )
@@ -1387,8 +1424,9 @@ def test_excel_upload_file_extension_invalid(
     """
     _ = mocker.patch.object(UploadCommand, "run")
     response = client.post(
-        "/api/v1/database/1/excel_upload/",
+        "/api/v1/database/1/upload/",
         data={
+            "type": "excel",
             "file": create_excel_file(filename=filename),
             "table_name": "table1",
         },
@@ -1403,6 +1441,7 @@ def test_excel_upload_file_extension_invalid(
     [
         (
             {
+                "type": "columnar",
                 "file": (create_columnar_file(), "out.parquet"),
                 "table_name": "table1",
             },
@@ -1415,6 +1454,7 @@ def test_excel_upload_file_extension_invalid(
             ),
             (
                 {
+                    "type": "columnar",
                     "already_exists": "fail",
                     "file": ANY,
                     "table_name": "table1",
@@ -1423,6 +1463,7 @@ def test_excel_upload_file_extension_invalid(
         ),
         (
             {
+                "type": "columnar",
                 "file": (create_columnar_file(), "out.parquet"),
                 "table_name": "table2",
                 "already_exists": "replace",
@@ -1439,6 +1480,7 @@ def test_excel_upload_file_extension_invalid(
             ),
             (
                 {
+                    "type": "columnar",
                     "already_exists": "replace",
                     "columns_read": ["col1", "col2"],
                     "file": ANY,
@@ -1467,7 +1509,7 @@ def test_columnar_upload(
     reader_mock = mocker.patch.object(ColumnarReader, "__init__")
     reader_mock.return_value = None
     response = client.post(
-        "/api/v1/database/1/columnar_upload/",
+        "/api/v1/database/1/upload/",
         data=payload,
         content_type="multipart/form-data",
     )
@@ -1482,6 +1524,7 @@ def test_columnar_upload(
     [
         (
             {
+                "type": "columnar",
                 "file": (create_columnar_file(), "out.parquet"),
                 "already_exists": "fail",
             },
@@ -1489,6 +1532,7 @@ def test_columnar_upload(
         ),
         (
             {
+                "type": "columnar",
                 "file": (create_columnar_file(), "out.parquet"),
                 "table_name": "",
                 "already_exists": "fail",
@@ -1496,11 +1540,12 @@ def test_columnar_upload(
             {"message": {"table_name": ["Length must be between 1 and 
10000."]}},
         ),
         (
-            {"table_name": "table1", "already_exists": "fail"},
+            {"type": "columnar", "table_name": "table1", "already_exists": 
"fail"},
             {"message": {"file": ["Field may not be null."]}},
         ),
         (
             {
+                "type": "columnar",
                 "file": "xpto",
                 "table_name": "table1",
                 "already_exists": "fail",
@@ -1509,6 +1554,7 @@ def test_columnar_upload(
         ),
         (
             {
+                "type": "columnar",
                 "file": (create_columnar_file(), "out.parquet"),
                 "table_name": "table1",
                 "already_exists": "xpto",
@@ -1530,7 +1576,7 @@ def test_columnar_upload_validation(
     _ = mocker.patch.object(UploadCommand, "run")
 
     response = client.post(
-        "/api/v1/database/1/columnar_upload/",
+        "/api/v1/database/1/upload/",
         data=payload,
         content_type="multipart/form-data",
     )
@@ -1559,8 +1605,9 @@ def test_columnar_upload_file_extension_valid(
     """
     _ = mocker.patch.object(UploadCommand, "run")
     response = client.post(
-        "/api/v1/database/1/columnar_upload/",
+        "/api/v1/database/1/upload/",
         data={
+            "type": "columnar",
             "file": (create_columnar_file(), filename),
             "table_name": "table1",
         },
@@ -1595,8 +1642,9 @@ def test_columnar_upload_file_extension_invalid(
     """
     _ = mocker.patch.object(UploadCommand, "run")
     response = client.post(
-        "/api/v1/database/1/columnar_upload/",
+        "/api/v1/database/1/upload/",
         data={
+            "type": "columnar",
             "file": create_columnar_file(filename=filename),
             "table_name": "table1",
         },
@@ -1611,8 +1659,8 @@ def test_csv_metadata(
 ) -> None:
     _ = mocker.patch.object(CSVReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/csv_metadata/",
-        data={"file": create_csv_file()},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "csv", "file": create_csv_file()},
         content_type="multipart/form-data",
     )
     assert response.status_code == 200
@@ -1623,8 +1671,8 @@ def test_csv_metadata_bad_extension(
 ) -> None:
     _ = mocker.patch.object(CSVReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/csv_metadata/",
-        data={"file": create_csv_file(filename="test.out")},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "csv", "file": create_csv_file(filename="test.out")},
         content_type="multipart/form-data",
     )
     assert response.status_code == 400
@@ -1636,21 +1684,29 @@ def test_csv_metadata_validation(
 ) -> None:
     _ = mocker.patch.object(CSVReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/csv_metadata/",
-        data={},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "csv"},
         content_type="multipart/form-data",
     )
     assert response.status_code == 400
     assert response.json == {"message": {"file": ["Field may not be null."]}}
 
+    response = client.post(
+        "/api/v1/database/upload_metadata/",
+        data={"file": create_csv_file(filename="test.csv")},
+        content_type="multipart/form-data",
+    )
+    assert response.status_code == 400
+    assert response.json == {"message": {"type": ["Missing data for required 
field."]}}
+
 
 def test_excel_metadata(
     mocker: MockerFixture, client: Any, full_api_access: None
 ) -> None:
     _ = mocker.patch.object(ExcelReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/excel_metadata/",
-        data={"file": create_excel_file()},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "excel", "file": create_excel_file()},
         content_type="multipart/form-data",
     )
     assert response.status_code == 200
@@ -1661,8 +1717,8 @@ def test_excel_metadata_bad_extension(
 ) -> None:
     _ = mocker.patch.object(ExcelReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/excel_metadata/",
-        data={"file": create_excel_file(filename="test.out")},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "excel", "file": create_excel_file(filename="test.out")},
         content_type="multipart/form-data",
     )
     assert response.status_code == 400
@@ -1674,8 +1730,8 @@ def test_excel_metadata_validation(
 ) -> None:
     _ = mocker.patch.object(ExcelReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/excel_metadata/",
-        data={},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "excel"},
         content_type="multipart/form-data",
     )
     assert response.status_code == 400
@@ -1687,8 +1743,8 @@ def test_columnar_metadata(
 ) -> None:
     _ = mocker.patch.object(ColumnarReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/columnar_metadata/",
-        data={"file": create_columnar_file()},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "columnar", "file": create_columnar_file()},
         content_type="multipart/form-data",
     )
     assert response.status_code == 200
@@ -1699,8 +1755,8 @@ def test_columnar_metadata_bad_extension(
 ) -> None:
     _ = mocker.patch.object(ColumnarReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/columnar_metadata/",
-        data={"file": create_columnar_file(filename="test.out")},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "columnar", "file": 
create_columnar_file(filename="test.out")},
         content_type="multipart/form-data",
     )
     assert response.status_code == 400
@@ -1712,8 +1768,8 @@ def test_columnar_metadata_validation(
 ) -> None:
     _ = mocker.patch.object(ColumnarReader, "file_metadata")
     response = client.post(
-        "/api/v1/database/columnar_metadata/",
-        data={},
+        "/api/v1/database/upload_metadata/",
+        data={"type": "columnar"},
         content_type="multipart/form-data",
     )
     assert response.status_code == 400

Reply via email to