diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx index 3fe4f3b7c6a..e4f7e4f6626 100644 --- a/superset-frontend/src/components/DatabaseSelector/index.tsx +++ b/superset-frontend/src/components/DatabaseSelector/index.tsx @@ -191,6 +191,8 @@ export function DatabaseSelector({ }: DatabaseSelectorProps) { const showCatalogSelector = !!db?.allow_multi_catalog; const [currentDb, setCurrentDb] = useState(); + const showSchemaSelector = + (db?.supports_schemas ?? currentDb?.supports_schemas) !== false; const [errorPayload, setErrorPayload] = useState(); const [currentCatalog, setCurrentCatalog] = useState< CatalogOption | null | undefined @@ -260,6 +262,12 @@ export function DatabaseSelector({ database_name: row.database_name, backend: row.backend, allow_multi_catalog: row.allow_multi_catalog, + supports_schemas: + ( + row as DatabaseObject & { + engine_information?: { supports_schemas?: boolean }; + } + ).engine_information?.supports_schemas !== false, order, })); @@ -597,7 +605,7 @@ export function DatabaseSelector({ {renderDatabaseSelect()} {renderError()} {showCatalogSelector && renderCatalogSelect()} - {renderSchemaSelect()} + {showSchemaSelector && renderSchemaSelect()} ); } diff --git a/superset-frontend/src/components/DatabaseSelector/types.ts b/superset-frontend/src/components/DatabaseSelector/types.ts index 1fef4b67763..5b10cae8d35 100644 --- a/superset-frontend/src/components/DatabaseSelector/types.ts +++ b/superset-frontend/src/components/DatabaseSelector/types.ts @@ -24,6 +24,7 @@ export type DatabaseValue = { id: number; database_name: string; backend?: string; + supports_schemas?: boolean; }; export type DatabaseObject = { @@ -31,6 +32,7 @@ export type DatabaseObject = { database_name: string; backend?: string; allow_multi_catalog?: boolean; + supports_schemas?: boolean; }; export interface DatabaseSelectorProps { diff --git a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx index e92e48ac6a0..c9f59361f10 100644 --- a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx +++ b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx @@ -260,6 +260,52 @@ test('table multi select retain all the values selected', async () => { expect(selections[1]).toHaveTextContent('table_c'); }); +test('calls onTableSelectChange for schema-less database without schema', async () => { + fetchMock.get(catalogApiRoute, { result: [] }); + fetchMock.get(schemaApiRoute, { result: [] }); + fetchMock.get(tablesApiRoute, getTableMockFunction()); + + const callback = jest.fn(); + const props = createProps({ + database: { + id: 1, + database_name: 'ydb', + backend: 'ydb', + supports_schemas: false, + }, + schema: undefined, + onTableSelectChange: callback, + }); + + render(, { useRedux: true, store }); + + const tableSelect = screen.getByRole('combobox', { + name: 'Select table or type to search tables', + }); + + await act(async () => { + await userEvent.click(tableSelect); + }); + + await waitFor( + () => { + expect(screen.getByText('table_a')).toBeInTheDocument(); + }, + { timeout: 10000 }, + ); + + await act(async () => { + await userEvent.click(screen.getByText('table_a')); + }); + + await waitFor( + () => { + expect(callback).toHaveBeenCalled(); + }, + { timeout: 10000 }, + ); +}, 15000); + test('TableOption renders correct icons for different table types', () => { // Test regular table const tableTable = { diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx index 69b17bffcf8..c6a593c2982 100644 --- a/superset-frontend/src/components/TableSelector/index.tsx +++ b/superset-frontend/src/components/TableSelector/index.tsx @@ -190,6 +190,7 @@ const TableSelector: FunctionComponent = ({ dbId: database?.id, catalog: currentCatalog, schema: currentSchema, + supportsSchemas: database?.supports_schemas, onSuccess: (data, isFetched) => { setErrorPayload(null); if (isFetched) { @@ -247,7 +248,8 @@ const TableSelector: FunctionComponent = ({ const internalTableChange = ( selectedOptions: TableOption | TableOption[] | undefined, ) => { - if (currentSchema) { + setTableSelectValue(selectedOptions); + if (currentSchema || database?.supports_schemas === false) { onTableSelectChange?.( Array.isArray(selectedOptions) ? selectedOptions.map(option => option?.value) @@ -255,8 +257,6 @@ const TableSelector: FunctionComponent = ({ currentCatalog, currentSchema, ); - } else { - setTableSelectValue(selectedOptions); } }; @@ -302,7 +302,8 @@ const TableSelector: FunctionComponent = ({ ); function renderTableSelect() { - const disabled = (currentSchema && !formMode && readOnly) || !currentSchema; + const disabled = + readOnly || (database?.supports_schemas !== false && !currentSchema); const label = t('Table'); diff --git a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanelWrapper.test.tsx b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanelWrapper.test.tsx new file mode 100644 index 00000000000..66d02d22874 --- /dev/null +++ b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanelWrapper.test.tsx @@ -0,0 +1,59 @@ +/** + * 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. + */ +import { render, waitFor } from 'spec/helpers/testing-library'; +import { SupersetClient } from '@superset-ui/core'; +import DatasetPanelWrapper from 'src/features/datasets/AddDataset/DatasetPanel'; + +jest.mock( + '@superset-ui/core/components/Icons/AsyncIcon', + () => + ({ fileName }: { fileName: string }) => ( + + ), +); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('fetches table metadata for schema-less database without schema', async () => { + const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({ + json: { + name: 'my_table', + columns: [{ name: 'id', type: 'INTEGER', longType: 'INTEGER' }], + }, + } as any); + + render( + , + { useRouter: true }, + ); + + await waitFor(() => { + expect(getSpy).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: expect.stringContaining('/api/v1/database/1/table_metadata/'), + }), + ); + }); +}); diff --git a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx index 661650b720c..f41ee9020fc 100644 --- a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx +++ b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx @@ -22,6 +22,7 @@ import { SupersetClient } from '@superset-ui/core'; import { logging } from '@apache-superset/core/utils'; import { DatasetObject } from 'src/features/datasets/AddDataset/types'; import { addDangerToast } from 'src/components/MessageToasts/actions'; +import { type DatabaseObject } from 'src/components'; import { toQueryString } from 'src/utils/urlUtils'; import DatasetPanel from './DatasetPanel'; import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types'; @@ -39,9 +40,9 @@ interface IColumnProps { */ tableName: string; /** - * Name of the schema + * Name of the schema (optional for databases that don't support schemas) */ - schema: string; + schema?: string | null; } export interface IDatasetPanelWrapperProps { @@ -58,6 +59,10 @@ export interface IDatasetPanelWrapperProps { */ catalog?: string | null; schema?: string | null; + /** + * The selected database object (used to check engine capabilities) + */ + database?: Partial | null; setHasColumns?: Function; datasets?: DatasetObject[] | undefined; } @@ -67,6 +72,7 @@ const DatasetPanelWrapper = ({ dbId, catalog, schema, + database, setHasColumns, datasets, }: IDatasetPanelWrapperProps) => { @@ -128,12 +134,13 @@ const DatasetPanelWrapper = ({ useEffect(() => { tableNameRef.current = tableName; - if (tableName && schema && dbId) { - getTableMetadata({ tableName, dbId, schema }); + const schemaRequired = database?.supports_schemas !== false; + if (tableName && dbId && (schema || !schemaRequired)) { + getTableMetadata({ tableName, dbId, schema: schema || undefined }); } // getTableMetadata is a const and should not be in dependency array // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tableName, dbId, schema]); + }, [tableName, dbId, schema, database]); return ( { expect(result.current.datasetNames).toEqual([]); }); +test('useDatasetsList fetches datasets for schema-less databases without schema filter', async () => { + const schemalessDb = { + id: 2, + database_name: 'ydb', + owners: [1] as [number], + supports_schemas: false, + }; + + const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({ + json: { + count: 1, + result: [{ id: 10, table_name: 'my_table', schema: null }], + }, + } as unknown as JsonResponse); + + const { result } = renderHook(() => useDatasetsList(schemalessDb, null)); + + await waitFor(() => { + expect(result.current.datasets).toHaveLength(1); + }); + + expect(result.current.datasetNames).toEqual(['my_table']); + expect(getSpy).toHaveBeenCalledTimes(1); + + // Verify the API was called without a schema filter + const callArg = getSpy.mock.calls[0]?.[0]?.endpoint; + expect(callArg).toBeDefined(); + + const risonParam = new URL(callArg!, 'http://localhost').searchParams.get( + 'q', + ); + expect(risonParam).toBeTruthy(); + const decoded = rison.decode(risonParam!) as { + filters: Array<{ col: string; opr: string; value: unknown }>; + }; + + // Only database filter and sql filter — no schema filter + const schemaFilter = decoded.filters.find(f => f.col === 'schema'); + expect(schemaFilter).toBeUndefined(); + + const dbFilter = decoded.filters.find(f => f.col === 'database'); + expect(dbFilter).toEqual({ col: 'database', opr: 'rel_o_m', value: 2 }); +}); + +test('useDatasetsList skips fetching when schema-less database id is undefined', () => { + const getSpy = jest.spyOn(SupersetClient, 'get'); + + const schemalessDb = { + database_name: 'ydb', + owners: [1] as [number], + supports_schemas: false, + } as typeof mockDb & { supports_schemas: boolean }; + + const { result } = renderHook(() => useDatasetsList(schemalessDb, null)); + + // No db.id — should NOT call API even for schema-less DB + expect(getSpy).not.toHaveBeenCalled(); + expect(result.current.datasets).toEqual([]); +}); + test('useDatasetsList encodes schemas with spaces and special characters in endpoint URL', async () => { const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({ json: { count: 0, result: [] }, diff --git a/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts b/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts index 96241fe5b61..b2f925b1759 100644 --- a/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts +++ b/superset-frontend/src/features/datasets/hooks/useDatasetLists.ts @@ -37,7 +37,8 @@ const useDatasetsList = ( schema: string | null | undefined, ) => { const [datasets, setDatasets] = useState([]); - const encodedSchema = schema ? encodeURIComponent(schema) : undefined; + const supportsSchemas = db?.supports_schemas !== false; + const encodedSchema = schema ? encodeURIComponent(schema) : null; const getDatasetsList = useCallback(async (filters: object[]) => { let results: DatasetObject[] = []; @@ -77,14 +78,16 @@ const useDatasetsList = ( useEffect(() => { const filters = [ { col: 'database', opr: 'rel_o_m', value: db?.id }, - { col: 'schema', opr: 'eq', value: encodedSchema }, + ...(supportsSchemas + ? [{ col: 'schema', opr: 'eq', value: encodedSchema }] + : []), { col: 'sql', opr: 'dataset_is_null_or_empty', value: true }, ]; - if (schema && db?.id !== undefined) { + if (db?.id !== undefined && (schema || !supportsSchemas)) { getDatasetsList(filters); } - }, [db?.id, schema, encodedSchema, getDatasetsList]); + }, [db?.id, schema, encodedSchema, supportsSchemas, getDatasetsList]); const datasetNames = useMemo( () => datasets?.map(dataset => dataset.table_name), diff --git a/superset-frontend/src/hooks/apiResources/tables.test.ts b/superset-frontend/src/hooks/apiResources/tables.test.ts index 572f5c7ca3a..d2f80fe0a26 100644 --- a/superset-frontend/src/hooks/apiResources/tables.test.ts +++ b/superset-frontend/src/hooks/apiResources/tables.test.ts @@ -240,6 +240,35 @@ describe('useTables hook', () => { expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1); }); + test('fetches tables without schema when supportsSchemas is false', async () => { + const expectDbId = 'db1'; + const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; + fetchMock.get(tableApiRoute, fakeApiResult); + fetchMock.get(`glob:*/api/v1/database/${expectDbId}/catalogs/*`, { + count: 0, + result: [], + }); + fetchMock.get(`glob:*/api/v1/database/${expectDbId}/schemas/*`, { + result: fakeSchemaApiResult, + }); + const { result, waitFor } = renderHook( + () => + useTables({ + dbId: expectDbId, + supportsSchemas: false, + }), + { + wrapper: createWrapper({ + useRedux: true, + store, + }), + }, + ); + // Tables are fetched even though no schema is provided or validated against schemaOptions + await waitFor(() => expect(result.current.data).toEqual(expectedData)); + expect(fetchMock.callHistory.calls(tableApiRoute).length).toBe(1); + }); + test('returns refreshed data after expires', async () => { const expectDbId = 'db1'; const expectedSchema = 'schema1'; diff --git a/superset-frontend/src/hooks/apiResources/tables.ts b/superset-frontend/src/hooks/apiResources/tables.ts index 99fc568d1f0..e8b59326604 100644 --- a/superset-frontend/src/hooks/apiResources/tables.ts +++ b/superset-frontend/src/hooks/apiResources/tables.ts @@ -96,7 +96,9 @@ type TableMetadataResponse = { export type TableExtendedMetadata = Record; -type Params = Omit; +type Params = Omit & { + supportsSchemas?: boolean; +}; const tableApi = api.injectEndpoints({ endpoints: builder => ({ @@ -166,7 +168,14 @@ export const { } = tableApi; export function useTables(options: Params) { - const { dbId, catalog, schema, onSuccess, onError } = options || {}; + const { + dbId, + catalog, + schema, + supportsSchemas = true, + onSuccess, + onError, + } = options || {}; const isMountedRef = useRef(false); const { currentData: schemaOptions, isFetching } = useSchemas({ dbId, @@ -177,9 +186,9 @@ export function useTables(options: Params) { [schemaOptions], ); - const enabled = Boolean( - dbId && schema && !isFetching && schemaOptionsMap.has(schema), - ); + const enabled = supportsSchemas + ? Boolean(dbId && schema && !isFetching && schemaOptionsMap.has(schema)) + : Boolean(dbId); const result = useTablesQuery( { dbId, catalog, schema, forceRefresh: false }, diff --git a/superset-frontend/src/pages/DatasetCreation/index.tsx b/superset-frontend/src/pages/DatasetCreation/index.tsx index 545dd3bf8a4..fb77e7eeb4b 100644 --- a/superset-frontend/src/pages/DatasetCreation/index.tsx +++ b/superset-frontend/src/pages/DatasetCreation/index.tsx @@ -122,6 +122,7 @@ export default function AddDataset() { dbId={dataset?.db?.id} catalog={dataset?.catalog} schema={dataset?.schema} + database={dataset?.db} setHasColumns={setHasColumns} datasets={datasets} /> diff --git a/superset/commands/database/tables.py b/superset/commands/database/tables.py index 31b3fd35427..0562d829077 100644 --- a/superset/commands/database/tables.py +++ b/superset/commands/database/tables.py @@ -44,7 +44,7 @@ class TablesDatabaseCommand(BaseCommand): self, db_id: int, catalog_name: str | None, - schema_name: str, + schema_name: str | None, force: bool, ): self._db_id = db_id @@ -55,6 +55,8 @@ class TablesDatabaseCommand(BaseCommand): def run(self) -> dict[str, Any]: self.validate() self._catalog_name = self._catalog_name or self._model.get_default_catalog() + if not self._model.db_engine_spec.supports_schemas: + self._schema_name = None try: tables = security_manager.get_datasources_accessible_by_user( database=self._model, diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py index da0dd3cf340..b399a25ad7c 100644 --- a/superset/databases/schemas.py +++ b/superset/databases/schemas.py @@ -1067,6 +1067,9 @@ class EngineInformationSchema(Schema): supports_oauth2 = fields.Boolean( metadata={"description": "The database supports OAuth2"} ) + supports_schemas = fields.Boolean( + metadata={"description": "The database uses schemas to organize tables"} + ) class DatabaseConnectionSchema(Schema): diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index 4d26ca8517a..de9c6c12302 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -577,6 +577,10 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods # Does the DB engine spec support cross-catalog queries? supports_cross_catalog_queries = False + # Does the DB engine support schemas? When set to False the schema selector is + # hidden in the dataset creation UI and schema is not required for table access. + supports_schemas = True + # Does the engine supports OAuth 2.0? This requires logic to be added to one of the # the user impersonation methods to handle personal tokens. supports_oauth2 = False @@ -2523,6 +2527,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods "disable_ssh_tunneling": cls.disable_ssh_tunneling, "supports_dynamic_catalog": cls.supports_dynamic_catalog, "supports_oauth2": cls.supports_oauth2, + "supports_schemas": cls.supports_schemas, } @classmethod diff --git a/superset/db_engine_specs/ydb.py b/superset/db_engine_specs/ydb.py index 9ea1b5dd195..5c3b051d970 100755 --- a/superset/db_engine_specs/ydb.py +++ b/superset/db_engine_specs/ydb.py @@ -51,6 +51,7 @@ class YDBEngineSpec(BaseEngineSpec): disable_ssh_tunneling = False supports_file_upload = False + supports_schemas = False allows_alias_in_orderby = True diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py index 7171a3174f2..03482ecbfd3 100644 --- a/tests/integration_tests/databases/api_tests.py +++ b/tests/integration_tests/databases/api_tests.py @@ -3427,6 +3427,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": True, "disable_ssh_tunneling": False, "supports_oauth2": False, + "supports_schemas": True, }, "supports_oauth2": False, }, @@ -3455,6 +3456,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": True, "disable_ssh_tunneling": True, "supports_oauth2": False, + "supports_schemas": True, }, "supports_oauth2": False, }, @@ -3513,6 +3515,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": False, "disable_ssh_tunneling": False, "supports_oauth2": False, + "supports_schemas": True, }, "supports_oauth2": False, }, @@ -3558,6 +3561,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": False, "disable_ssh_tunneling": True, "supports_oauth2": True, + "supports_schemas": True, }, "supports_oauth2": True, }, @@ -3616,6 +3620,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": False, "disable_ssh_tunneling": False, "supports_oauth2": False, + "supports_schemas": True, }, "supports_oauth2": False, }, @@ -3630,6 +3635,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": False, "disable_ssh_tunneling": False, "supports_oauth2": False, + "supports_schemas": True, }, "supports_oauth2": False, }, @@ -3664,6 +3670,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": False, "disable_ssh_tunneling": False, "supports_oauth2": False, + "supports_schemas": True, }, "supports_oauth2": False, }, @@ -3678,6 +3685,7 @@ class TestDatabaseApi(SupersetTestCase): "supports_dynamic_catalog": False, "disable_ssh_tunneling": False, "supports_oauth2": False, + "supports_schemas": True, }, "supports_oauth2": False, }, diff --git a/tests/unit_tests/commands/databases/tables_test.py b/tests/unit_tests/commands/databases/tables_test.py index 80590c7ada7..57fce9d3f80 100644 --- a/tests/unit_tests/commands/databases/tables_test.py +++ b/tests/unit_tests/commands/databases/tables_test.py @@ -148,6 +148,78 @@ def test_tables_with_catalog( ) +@pytest.fixture +def database_without_schema_support(mocker: MockerFixture) -> MagicMock: + """ + Mock a database that does not support schemas (e.g. YDB). + """ + mocker.patch("superset.commands.database.tables.db") + + database = mocker.MagicMock() + database.database_name = "test_database" + database.get_default_catalog.return_value = None + database.db_engine_spec.supports_schemas = False + database.get_all_table_names_in_schema.return_value = { + ("table1", None, None), + ("table2", None, None), + } + database.get_all_view_names_in_schema.return_value = set() + database.get_all_materialized_view_names_in_schema.return_value = set() + + DatabaseDAO = mocker.patch("superset.commands.database.tables.DatabaseDAO") # noqa: N806 + DatabaseDAO.find_by_id.return_value = database + + return database + + +def test_tables_without_schema_support( + mocker: MockerFixture, + database_without_schema_support: MagicMock, +) -> None: + """ + Test that schema is overridden to None for databases that don't support schemas. + Any schema name passed to the command is ignored. + """ + get_datasources_accessible_by_user = mocker.patch.object( + security_manager, + "get_datasources_accessible_by_user", + side_effect=[ + { + DatasourceName("table1", None), # type: ignore[arg-type] + DatasourceName("table2", None), # type: ignore[arg-type] + }, + set(), # Empty set for views + set(), # Empty set for materialized views + ], + ) + + db = mocker.patch("superset.commands.database.tables.db") + db.session.query().filter().options().all.return_value = [] + + # Schema name should be overridden to None when supports_schemas=False + payload = TablesDatabaseCommand(1, None, "any_schema", False).run() + + assert payload["count"] == 2 + assert {item["value"] for item in payload["result"]} == {"table1", "table2"} + + # Verify schema was set to None when calling the underlying DB methods + database_without_schema_support.get_all_table_names_in_schema.assert_called_with( + catalog=None, + schema=None, + force=False, + cache=database_without_schema_support.table_cache_enabled, + cache_timeout=database_without_schema_support.table_cache_timeout, + ) + + # Verify security_manager was called with schema=None + get_datasources_accessible_by_user.assert_any_call( + database=database_without_schema_support, + catalog=None, + schema=None, + datasource_names=mocker.ANY, + ) + + def test_tables_without_catalog( mocker: MockerFixture, database_without_catalog: MockerFixture, diff --git a/tests/unit_tests/databases/api_test.py b/tests/unit_tests/databases/api_test.py index 853b71b2a8f..db15d1c1d13 100644 --- a/tests/unit_tests/databases/api_test.py +++ b/tests/unit_tests/databases/api_test.py @@ -243,6 +243,7 @@ def test_database_connection( "supports_dynamic_catalog": False, "supports_file_upload": True, "supports_oauth2": True, + "supports_schemas": True, }, "expose_in_sqllab": True, "extra": '{\n "metadata_params": {},\n "engine_params": {},\n "metadata_cache_timeout": {},\n "schemas_allowed_for_file_upload": []\n}\n', # noqa: E501 @@ -332,6 +333,7 @@ def test_database_connection( "supports_dynamic_catalog": False, "supports_file_upload": True, "supports_oauth2": True, + "supports_schemas": True, }, "expose_in_sqllab": True, "force_ctas_schema": None,