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

beto 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 ce668d46cc feat(SIP-95): catalogs in SQL Lab and datasets (#28376)
ce668d46cc is described below

commit ce668d46cc5d429a249fdd9e091650457da20361
Author: Beto Dealmeida <[email protected]>
AuthorDate: Wed May 8 17:19:36 2024 -0400

    feat(SIP-95): catalogs in SQL Lab and datasets (#28376)
---
 .../superset-ui-core/src/query/types/Query.ts      |   1 +
 .../superset-ui-core/src/ui-overrides/types.ts     |   1 +
 superset-frontend/src/SqlLab/actions/sqlLab.js     |  30 +++++-
 .../src/SqlLab/actions/sqlLab.test.js              |  31 +++++-
 .../SqlLab/components/AceEditorWrapper/index.tsx   |   3 +
 .../AceEditorWrapper/useKeywords.test.ts           |  15 ++-
 .../components/AceEditorWrapper/useKeywords.ts     |   9 +-
 .../SaveDatasetModal/SaveDatasetModal.test.tsx     |  32 +++++++
 .../SqlLab/components/SaveDatasetModal/index.tsx   |   2 +
 .../SqlLab/components/SaveQuery/SaveQuery.test.tsx |   1 +
 .../src/SqlLab/components/SaveQuery/index.tsx      |   4 +-
 .../SqlEditorLeftBar/SqlEditorLeftBar.test.tsx     |  61 ++++++++++--
 .../SqlLab/components/SqlEditorLeftBar/index.tsx   |  34 +++++--
 .../SqlLab/components/TabbedSqlEditors/index.tsx   |   2 +
 .../src/SqlLab/components/TableElement/index.tsx   |   4 +-
 superset-frontend/src/SqlLab/fixtures.ts           |  15 +++
 .../src/SqlLab/reducers/getInitialState.ts         |   2 +
 superset-frontend/src/SqlLab/reducers/sqlLab.js    |  14 +++
 superset-frontend/src/SqlLab/types.ts              |   2 +
 .../SqlLab/utils/reduxStateToLocalStorageHelper.ts |  38 ++++----
 .../DatabaseSelector/DatabaseSelector.test.tsx     |   8 ++
 .../src/components/DatabaseSelector/index.tsx      | 104 +++++++++++++++++++--
 .../src/components/Datasource/DatasourceEditor.jsx |  13 +++
 .../TableSelector/TableSelector.test.tsx           |   7 ++
 .../src/components/TableSelector/index.tsx         |  36 ++++++-
 .../DndColumnSelectControl/DndFilterSelect.tsx     |  10 +-
 .../FilterControl/AdhocFilterControl/index.jsx     |  10 +-
 superset-frontend/src/explore/types.ts             |   1 +
 .../databases/DatabaseModal/ExtraOptions.tsx       |  23 ++++-
 .../DatabaseModal/SSHTunnelSwitch.test.tsx         |   1 +
 .../UploadDataModel/UploadDataModal.test.tsx       |   4 +
 superset-frontend/src/features/databases/types.ts  |   3 +
 .../datasets/AddDataset/DatasetPanel/index.tsx     |  13 ++-
 .../features/datasets/AddDataset/Footer/index.tsx  |   1 +
 .../datasets/AddDataset/LeftPanel/index.tsx        |  10 ++
 .../src/features/datasets/AddDataset/types.tsx     |   3 +
 .../hooks/apiResources/{schemas.ts => catalogs.ts} |  43 +++++----
 superset-frontend/src/hooks/apiResources/index.ts  |   1 +
 .../src/hooks/apiResources/queryApi.ts             |   1 +
 .../src/hooks/apiResources/queryValidations.ts     |   4 +-
 .../src/hooks/apiResources/schemas.test.ts         |  10 +-
 .../src/hooks/apiResources/schemas.ts              |  31 ++++--
 .../src/hooks/apiResources/sqlEditorTabs.ts        |   2 +
 superset-frontend/src/hooks/apiResources/sqlLab.ts |   2 +
 .../src/hooks/apiResources/tables.test.ts          |  16 ++++
 superset-frontend/src/hooks/apiResources/tables.ts |  35 ++++---
 .../src/pages/DatasetCreation/index.tsx            |   9 ++
 superset-frontend/src/types/Database.ts            |   1 +
 superset-frontend/src/utils/datasourceUtils.js     |   1 +
 superset-frontend/src/utils/urlUtils.test.ts       |  46 ++++++++-
 superset-frontend/src/utils/urlUtils.ts            |  13 +++
 superset/cachekeys/api.py                          |   1 +
 superset/commands/dashboard/importers/v0.py        |   1 +
 superset/commands/database/validate_sql.py         |   2 +-
 superset/connectors/sqla/models.py                 |  10 +-
 superset/dashboards/schemas.py                     |   1 +
 superset/databases/api.py                          |   1 +
 superset/databases/schemas.py                      |  22 ++++-
 superset/datasets/api.py                           |  10 +-
 superset/datasets/schemas.py                       |   7 ++
 superset/db_engine_specs/base.py                   |   1 +
 superset/models/core.py                            |   5 +
 superset/models/sql_lab.py                         |   1 +
 superset/queries/saved_queries/api.py              |  16 +++-
 superset/sqllab/schemas.py                         |   1 +
 superset/sqllab/sqllab_execution_context.py        |   4 +
 superset/sqllab/utils.py                           |   1 +
 superset/views/database/mixins.py                  |   4 +-
 superset/views/datasource/schemas.py               |   3 +
 superset/views/datasource/views.py                 |   1 +
 superset/views/sql_lab/views.py                    |   1 +
 71 files changed, 735 insertions(+), 121 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts 
b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
index 4d5ccaf6f0..b6b1fd3a63 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
@@ -316,6 +316,7 @@ export type Query = {
   link?: string;
   progress: number;
   resultsKey: string | null;
+  catalog?: string | null;
   schema?: string;
   sql: string;
   sqlEditorId: string;
diff --git 
a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts 
b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
index f8f10982bd..736a57da90 100644
--- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
@@ -168,6 +168,7 @@ export interface SubMenuProps {
 export interface CustomAutoCompleteArgs {
   queryEditorId: string;
   dbId?: string | number;
+  catalog?: string | null;
   schema?: string;
 }
 
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js 
b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 6befa17ac1..a153c4eb45 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -55,6 +55,7 @@ export const REMOVE_QUERY = 'REMOVE_QUERY';
 export const EXPAND_TABLE = 'EXPAND_TABLE';
 export const COLLAPSE_TABLE = 'COLLAPSE_TABLE';
 export const QUERY_EDITOR_SETDB = 'QUERY_EDITOR_SETDB';
+export const QUERY_EDITOR_SET_CATALOG = 'QUERY_EDITOR_SET_CATALOG';
 export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA';
 export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE';
 export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN';
@@ -326,6 +327,7 @@ export function runQuery(query) {
       database_id: query.dbId,
       json: true,
       runAsync: query.runAsync,
+      catalog: query.catalog,
       schema: query.schema,
       sql: query.sql,
       sql_editor_id: query.sqlEditorId,
@@ -381,6 +383,7 @@ export function runQueryFromSqlEditor(
       sql: qe.selectedText || qe.sql,
       sqlEditorId: qe.id,
       tab: qe.name,
+      catalog: qe.catalog,
       schema: qe.schema,
       tempTable,
       templateParams: qe.templateParams,
@@ -556,7 +559,7 @@ export function addNewQueryEditor() {
     );
     const dbIds = Object.values(databases).map(database => database.id);
     const firstDbId = dbIds.length > 0 ? Math.min(...dbIds) : undefined;
-    const { dbId, schema, queryLimit, autorun } = {
+    const { dbId, catalog, schema, queryLimit, autorun } = {
       ...queryEditors[0],
       ...activeQueryEditor,
       ...(unsavedQueryEditor.id === activeQueryEditor?.id &&
@@ -578,6 +581,7 @@ export function addNewQueryEditor() {
     return dispatch(
       addQueryEditor({
         dbId: dbId || defaultDbId || firstDbId,
+        catalog: catalog ?? null,
         schema: schema ?? null,
         autorun: autorun ?? false,
         sql: `${warning}SELECT ...`,
@@ -600,6 +604,7 @@ export function cloneQueryToNewTab(query, autorun) {
     const queryEditor = {
       name: t('Copy of %s', sourceQueryEditor.name),
       dbId: query.dbId ? query.dbId : null,
+      catalog: query.catalog ? query.catalog : null,
       schema: query.schema ? query.schema : null,
       autorun,
       sql: query.sql,
@@ -656,6 +661,7 @@ export function setTables(tableSchemas) {
       return {
         dbId: tableSchema.database_id,
         queryEditorId: tableSchema.tab_state_id.toString(),
+        catalog: tableSchema.catalog,
         schema: tableSchema.schema,
         name: tableSchema.table,
         expanded: tableSchema.expanded,
@@ -694,6 +700,7 @@ export function switchQueryEditor(queryEditor, 
displayLimit) {
             autorun: json.autorun,
             dbId: json.database_id,
             templateParams: json.template_params,
+            catalog: json.catalog,
             schema: json.schema,
             queryLimit: json.query_limit,
             remoteId: json.saved_query?.id,
@@ -797,6 +804,14 @@ export function queryEditorSetDb(queryEditor, dbId) {
   return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
 }
 
+export function queryEditorSetCatalog(queryEditor, catalog) {
+  return {
+    type: QUERY_EDITOR_SET_CATALOG,
+    queryEditor: queryEditor || {},
+    catalog,
+  };
+}
+
 export function queryEditorSetSchema(queryEditor, schema) {
   return {
     type: QUERY_EDITOR_SET_SCHEMA,
@@ -954,12 +969,13 @@ export function mergeTable(table, query, prepend) {
   return { type: MERGE_TABLE, table, query, prepend };
 }
 
-export function addTable(queryEditor, tableName, schemaName) {
+export function addTable(queryEditor, tableName, catalogName, schemaName) {
   return function (dispatch, getState) {
     const query = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
     const table = {
       dbId: query.dbId,
       queryEditorId: query.id,
+      catalog: catalogName,
       schema: schemaName,
       name: tableName,
     };
@@ -983,12 +999,14 @@ export function runTablePreviewQuery(newTable) {
       sqlLab: { databases },
     } = getState();
     const database = databases[newTable.dbId];
-    const { dbId } = newTable;
+    const { dbId, catalog, schema } = newTable;
 
     if (database && !database.disable_data_preview) {
       const dataPreviewQuery = {
         id: shortid.generate(),
         dbId,
+        catalog,
+        schema,
         sql: newTable.selectStar,
         tableName: newTable.name,
         sqlEditorId: null,
@@ -1003,6 +1021,7 @@ export function runTablePreviewQuery(newTable) {
             {
               id: newTable.id,
               dbId: newTable.dbId,
+              catalog: newTable.catalog,
               schema: newTable.schema,
               name: newTable.name,
               queryEditorId: newTable.queryEditorId,
@@ -1180,6 +1199,7 @@ export function popStoredQuery(urlId) {
           addQueryEditor({
             name: json.name ? json.name : t('Shared query'),
             dbId: json.dbId ? parseInt(json.dbId, 10) : null,
+            catalog: json.catalog ? json.catalog : null,
             schema: json.schema ? json.schema : null,
             autorun: json.autorun ? json.autorun : false,
             sql: json.sql ? json.sql : 'SELECT ...',
@@ -1215,6 +1235,7 @@ export function popQuery(queryId) {
         const queryData = json.result;
         const queryEditorProps = {
           dbId: queryData.database.id,
+          catalog: queryData.catalog,
           schema: queryData.schema,
           sql: queryData.sql,
           name: t('Copy of %s', queryData.tab_name),
@@ -1268,12 +1289,13 @@ export function createDatasourceFailed(err) {
 export function createDatasource(vizOptions) {
   return dispatch => {
     dispatch(createDatasourceStarted());
-    const { dbId, schema, datasourceName, sql } = vizOptions;
+    const { dbId, catalog, schema, datasourceName, sql } = vizOptions;
     return SupersetClient.post({
       endpoint: '/api/v1/dataset/',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
         database: dbId,
+        catalog,
         schema,
         sql,
         table_name: datasourceName,
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js 
b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index 871b3ff6f6..d6b70bf6a0 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -419,6 +419,7 @@ describe('async actions', () => {
           queryEditor: {
             name: 'Copy of Dummy query editor',
             dbId: 1,
+            catalog: query.catalog,
             schema: query.schema,
             autorun: true,
             sql: 'SELECT * FROM something',
@@ -481,6 +482,7 @@ describe('async actions', () => {
               sql: expect.stringContaining('SELECT ...'),
               name: `Untitled Query 7`,
               dbId: defaultQueryEditor.dbId,
+              catalog: defaultQueryEditor.catalog,
               schema: defaultQueryEditor.schema,
               autorun: false,
               queryLimit:
@@ -607,6 +609,24 @@ describe('async actions', () => {
       });
     });
 
+    describe('queryEditorSetCatalog', () => {
+      it('updates the tab state in the backend', () => {
+        expect.assertions(1);
+
+        const catalog = 'public';
+        const store = mockStore({});
+        const expectedActions = [
+          {
+            type: actions.QUERY_EDITOR_SET_CATALOG,
+            queryEditor,
+            catalog,
+          },
+        ];
+        store.dispatch(actions.queryEditorSetCatalog(queryEditor, catalog));
+        expect(store.getActions()).toEqual(expectedActions);
+      });
+    });
+
     describe('queryEditorSetSchema', () => {
       it('updates the tab state in the backend', () => {
         expect.assertions(1);
@@ -747,6 +767,7 @@ describe('async actions', () => {
     describe('addTable', () => {
       it('dispatches table state from unsaved change', () => {
         const tableName = 'table';
+        const catalogName = null;
         const schemaName = 'schema';
         const expectedDbId = 473892;
         const store = mockStore({
@@ -759,12 +780,18 @@ describe('async actions', () => {
             },
           },
         });
-        const request = actions.addTable(query, tableName, schemaName);
+        const request = actions.addTable(
+          query,
+          tableName,
+          catalogName,
+          schemaName,
+        );
         request(store.dispatch, store.getState);
         expect(store.getActions()[0]).toEqual(
           expect.objectContaining({
             table: expect.objectContaining({
               name: tableName,
+              catalog: catalogName,
               schema: schemaName,
               dbId: expectedDbId,
             }),
@@ -811,6 +838,7 @@ describe('async actions', () => {
         });
 
         const tableName = 'table';
+        const catalogName = null;
         const schemaName = 'schema';
         const store = mockStore({
           ...initialState,
@@ -829,6 +857,7 @@ describe('async actions', () => {
         const request = actions.runTablePreviewQuery({
           dbId: 1,
           name: tableName,
+          catalog: catalogName,
           schema: schemaName,
         });
         return request(store.dispatch, store.getState).then(() => {
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
index 354d6b1c8f..219a1f9bba 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
@@ -74,6 +74,7 @@ const AceEditorWrapper = ({
     'id',
     'dbId',
     'sql',
+    'catalog',
     'schema',
     'templateParams',
     'cursorPosition',
@@ -161,6 +162,7 @@ const AceEditorWrapper = ({
 
   const { data: annotations } = useAnnotations({
     dbId: queryEditor.dbId,
+    catalog: queryEditor.catalog,
     schema: queryEditor.schema,
     sql: currentSql,
     templateParams: queryEditor.templateParams,
@@ -170,6 +172,7 @@ const AceEditorWrapper = ({
     {
       queryEditorId,
       dbId: queryEditor.dbId,
+      catalog: queryEditor.catalog,
       schema: queryEditor.schema,
     },
     !autocomplete,
diff --git 
a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
index adee7d6818..193e715fb2 100644
--- 
a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
+++ 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
@@ -189,7 +189,12 @@ test('returns column keywords among selected tables', 
async () => {
     storeWithSqlLab.dispatch(
       tableApiUtil.upsertQueryData(
         'tableMetadata',
-        { dbId: expectDbId, schema: expectSchema, table: expectTable },
+        {
+          dbId: expectDbId,
+          catalog: null,
+          schema: expectSchema,
+          table: expectTable,
+        },
         {
           name: expectTable,
           columns: [
@@ -205,7 +210,12 @@ test('returns column keywords among selected tables', 
async () => {
     storeWithSqlLab.dispatch(
       tableApiUtil.upsertQueryData(
         'tableMetadata',
-        { dbId: expectDbId, schema: expectSchema, table: unexpectedTable },
+        {
+          dbId: expectDbId,
+          catalog: null,
+          schema: expectSchema,
+          table: unexpectedTable,
+        },
         {
           name: unexpectedTable,
           columns: [
@@ -227,6 +237,7 @@ test('returns column keywords among selected tables', async 
() => {
       useKeywords({
         queryEditorId: expectQueryEditorId,
         dbId: expectDbId,
+        catalog: null,
         schema: expectSchema,
       }),
     {
diff --git 
a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
index 09a5035260..2eac21ff39 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
@@ -42,6 +42,7 @@ import { SqlLabRootState } from 'src/SqlLab/types';
 type Params = {
   queryEditorId: string | number;
   dbId?: string | number;
+  catalog?: string | null;
   schema?: string;
 };
 
@@ -58,7 +59,7 @@ const getHelperText = (value: string) =>
 const extensionsRegistry = getExtensionsRegistry();
 
 export function useKeywords(
-  { queryEditorId, dbId, schema }: Params,
+  { queryEditorId, dbId, catalog, schema }: Params,
   skip = false,
 ) {
   const useCustomKeywords = extensionsRegistry.get(
@@ -68,6 +69,7 @@ export function useKeywords(
   const customKeywords = useCustomKeywords?.({
     queryEditorId: String(queryEditorId),
     dbId,
+    catalog,
     schema,
   });
   const dispatch = useDispatch();
@@ -78,6 +80,7 @@ export function useKeywords(
   const { data: schemaOptions } = useSchemasQueryState(
     {
       dbId,
+      catalog: catalog || undefined,
       forceRefresh: false,
     },
     { skip: skipFetch || !dbId },
@@ -85,6 +88,7 @@ export function useKeywords(
   const { data: tableData } = useTablesQueryState(
     {
       dbId,
+      catalog,
       schema,
       forceRefresh: false,
     },
@@ -125,6 +129,7 @@ export function useKeywords(
           dbId && schema
             ? {
                 dbId,
+                catalog,
                 schema,
                 table,
               }
@@ -137,7 +142,7 @@ export function useKeywords(
         });
     });
     return [...columns];
-  }, [dbId, schema, apiState, tablesForColumnMetadata]);
+  }, [dbId, catalog, schema, apiState, tablesForColumnMetadata]);
 
   const insertMatch = useEffectEvent((editor: Editor, data: any) => {
     if (data.meta === 'table') {
diff --git 
a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
index 8568bf2080..c3d1658f3a 100644
--- 
a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
@@ -210,6 +210,38 @@ describe('SaveDatasetModal', () => {
     expect(createDatasource).toHaveBeenCalledWith({
       datasourceName: 'my dataset',
       dbId: 1,
+      catalog: null,
+      schema: 'main',
+      sql: 'SELECT *',
+      templateParams: undefined,
+    });
+  });
+
+  it('sends the catalog when creating the dataset', async () => {
+    const dummyDispatch = jest.fn().mockResolvedValue({});
+    useDispatchMock.mockReturnValue(dummyDispatch);
+    useSelectorMock.mockReturnValue({ ...user });
+
+    render(
+      <SaveDatasetModal
+        {...mockedProps}
+        datasource={{ ...mockedProps.datasource, catalog: 'public' }}
+      />,
+      { useRedux: true },
+    );
+
+    const inputFieldText = screen.getByDisplayValue(/unimportant/i);
+    fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
+
+    const saveConfirmationBtn = screen.getByRole('button', {
+      name: /save/i,
+    });
+    userEvent.click(saveConfirmationBtn);
+
+    expect(createDatasource).toHaveBeenCalledWith({
+      datasourceName: 'my dataset',
+      dbId: 1,
+      catalog: 'public',
       schema: 'main',
       sql: 'SELECT *',
       templateParams: undefined,
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
index 885265fe8d..011d4a7d21 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
@@ -77,6 +77,7 @@ export interface ISaveableDatasource {
   dbId: number;
   sql: string;
   templateParams?: string | object | null;
+  catalog?: string | null;
   schema?: string | null;
   database?: Database;
 }
@@ -292,6 +293,7 @@ export const SaveDatasetModal = ({
       createDatasource({
         sql: datasource.sql,
         dbId: datasource.dbId || datasource?.database?.id,
+        catalog: datasource?.catalog,
         schema: datasource?.schema,
         templateParams,
         datasourceName: datasetName,
diff --git 
a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx 
b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
index 54b81df960..3d0413f62c 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.tsx
@@ -42,6 +42,7 @@ const mockState = {
       {
         id: mockedProps.queryEditorId,
         dbId: 1,
+        catalog: null,
         schema: 'main',
         sql: 'SELECT * FROM t',
       },
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx 
b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
index a7ac8b1b2a..2cb50b0c06 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
@@ -48,7 +48,7 @@ export type QueryPayload = {
   description?: string;
   id?: string;
   remoteId?: number;
-} & Pick<QueryEditor, 'dbId' | 'schema' | 'sql'>;
+} & Pick<QueryEditor, 'dbId' | 'catalog' | 'schema' | 'sql'>;
 
 const Styles = styled.span`
   span[role='img'] {
@@ -78,6 +78,7 @@ const SaveQuery = ({
     'dbId',
     'latestQueryId',
     'queryLimit',
+    'catalog',
     'schema',
     'selectedText',
     'sql',
@@ -115,6 +116,7 @@ const SaveQuery = ({
     description,
     dbId: query.dbId ?? 0,
     sql: query.sql,
+    catalog: query.catalog,
     schema: query.schema,
     templateParams: query.templateParams,
     remoteId: query?.remoteId || undefined,
diff --git 
a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
 
b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
index b5003b16f7..27d6c44d01 100644
--- 
a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.tsx
@@ -44,6 +44,10 @@ const mockedProps = {
 
 beforeEach(() => {
   fetchMock.get('glob:*/api/v1/database/?*', { result: [] });
+  fetchMock.get('glob:*/api/v1/database/*/catalogs/?*', {
+    count: 0,
+    result: [],
+  });
   fetchMock.get('glob:*/api/v1/database/*/schemas/?*', {
     count: 2,
     result: ['main', 'new_schema'],
@@ -103,11 +107,14 @@ test('renders a TableElement', async () => {
 });
 
 test('table should be visible when expanded is true', async () => {
-  const { container, getByText, getByRole, queryAllByText } =
-    await renderAndWait(mockedProps, undefined, {
+  const { container, getByText, getByRole } = await renderAndWait(
+    mockedProps,
+    undefined,
+    {
       ...initialState,
       sqlLab: { ...initialState.sqlLab, tables: [table] },
-    });
+    },
+  );
 
   const dbSelect = getByRole('combobox', {
     name: 'Select database or type to search databases',
@@ -115,14 +122,56 @@ test('table should be visible when expanded is true', 
async () => {
   const schemaSelect = getByRole('combobox', {
     name: 'Select schema or type to search schemas',
   });
-  const dropdown = getByText(/Table/i);
-  const abUser = queryAllByText(/ab_user/i);
+  const dropdown = getByText(/Select table/i);
+  const abUser = getByText(/ab_user/i);
+
+  expect(getByText(/Database/i)).toBeInTheDocument();
+  expect(dbSelect).toBeInTheDocument();
+  expect(schemaSelect).toBeInTheDocument();
+  expect(dropdown).toBeInTheDocument();
+  expect(abUser).toBeInTheDocument();
+  expect(
+    container.querySelector('.ant-collapse-content-active'),
+  ).toBeInTheDocument();
+  table.columns.forEach(({ name }) => {
+    expect(getByText(name)).toBeInTheDocument();
+  });
+});
+
+test('catalog selector should be visible when enabled in the database', async 
() => {
+  const { container, getByText, getByRole } = await renderAndWait(
+    {
+      ...mockedProps,
+      database: {
+        ...mockedProps.database,
+        allow_multi_catalog: true,
+      },
+    },
+    undefined,
+    {
+      ...initialState,
+      sqlLab: { ...initialState.sqlLab, tables: [table] },
+    },
+  );
+
+  const dbSelect = getByRole('combobox', {
+    name: 'Select database or type to search databases',
+  });
+  const catalogSelect = getByRole('combobox', {
+    name: 'Select catalog or type to search catalogs',
+  });
+  const schemaSelect = getByRole('combobox', {
+    name: 'Select schema or type to search schemas',
+  });
+  const dropdown = getByText(/Select table/i);
+  const abUser = getByText(/ab_user/i);
 
   expect(getByText(/Database/i)).toBeInTheDocument();
   expect(dbSelect).toBeInTheDocument();
+  expect(catalogSelect).toBeInTheDocument();
   expect(schemaSelect).toBeInTheDocument();
   expect(dropdown).toBeInTheDocument();
-  expect(abUser).toHaveLength(2);
+  expect(abUser).toBeInTheDocument();
   expect(
     container.querySelector('.ant-collapse-content-active'),
   ).toBeInTheDocument();
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx 
b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 15a1735626..1eee80f485 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -34,6 +34,7 @@ import {
   removeTables,
   collapseTable,
   expandTable,
+  queryEditorSetCatalog,
   queryEditorSetSchema,
   setDatabases,
   addDangerToast,
@@ -115,13 +116,17 @@ const SqlEditorLeftBar = ({
     shallowEqual,
   );
   const dispatch = useDispatch();
-  const queryEditor = useQueryEditor(queryEditorId, ['dbId', 'schema']);
+  const queryEditor = useQueryEditor(queryEditorId, [
+    'dbId',
+    'catalog',
+    'schema',
+  ]);
 
   const [emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
   const [userSelectedDb, setUserSelected] = useState<DatabaseObject | null>(
     null,
   );
-  const { schema } = queryEditor;
+  const { catalog, schema } = queryEditor;
 
   useEffect(() => {
     const bool = querystring.parse(window.location.search).db;
@@ -138,9 +143,9 @@ const SqlEditorLeftBar = ({
     }
   }, [database]);
 
-  const onEmptyResults = (searchText?: string) => {
+  const onEmptyResults = useCallback((searchText?: string) => {
     setEmptyResultsWithSearch(!!searchText);
-  };
+  }, []);
 
   const onDbChange = ({ id: dbId }: { id: number }) => {
     setEmptyState?.(false);
@@ -152,7 +157,11 @@ const SqlEditorLeftBar = ({
     [tables],
   );
 
-  const onTablesChange = (tableNames: string[], schemaName: string) => {
+  const onTablesChange = (
+    tableNames: string[],
+    catalogName: string | null,
+    schemaName: string,
+  ) => {
     if (!schemaName) {
       return;
     }
@@ -169,7 +178,7 @@ const SqlEditorLeftBar = ({
     });
 
     tablesToAdd.forEach(tableName => {
-      dispatch(addTable(queryEditor, tableName, schemaName));
+      dispatch(addTable(queryEditor, tableName, catalogName, schemaName));
     });
 
     dispatch(removeTables(currentTables));
@@ -210,6 +219,15 @@ const SqlEditorLeftBar = ({
   const shouldShowReset = window.location.search === '?reset=1';
   const tableMetaDataHeight = height - 130; // 130 is the height of the 
selects above
 
+  const handleCatalogChange = useCallback(
+    (catalog: string | null) => {
+      if (queryEditor) {
+        dispatch(queryEditorSetCatalog(queryEditor, catalog));
+      }
+    },
+    [dispatch, queryEditor],
+  );
+
   const handleSchemaChange = useCallback(
     (schema: string) => {
       if (queryEditor) {
@@ -246,9 +264,11 @@ const SqlEditorLeftBar = ({
         getDbList={handleDbList}
         handleError={handleError}
         onDbChange={onDbChange}
+        onCatalogChange={handleCatalogChange}
+        catalog={catalog}
         onSchemaChange={handleSchemaChange}
-        onTableSelectChange={onTablesChange}
         schema={schema}
+        onTableSelectChange={onTablesChange}
         tableValue={selectedTableNames}
         sqlLabMode
       />
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx 
b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
index 078276ad26..7b4db1cbe8 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
@@ -111,6 +111,7 @@ class TabbedSqlEditors extends 
React.PureComponent<TabbedSqlEditorsProps> {
       queryId,
       dbid,
       dbname,
+      catalog,
       schema,
       autorun,
       new: isNewQuery,
@@ -149,6 +150,7 @@ class TabbedSqlEditors extends 
React.PureComponent<TabbedSqlEditorsProps> {
         const newQueryEditor = {
           name,
           dbId: databaseId,
+          catalog,
           schema,
           autorun,
           sql,
diff --git a/superset-frontend/src/SqlLab/components/TableElement/index.tsx 
b/superset-frontend/src/SqlLab/components/TableElement/index.tsx
index e29a654c22..2f93491e88 100644
--- a/superset-frontend/src/SqlLab/components/TableElement/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TableElement/index.tsx
@@ -101,7 +101,7 @@ const StyledCollapsePanel = styled(Collapse.Panel)`
 `;
 
 const TableElement = ({ table, ...props }: TableElementProps) => {
-  const { dbId, schema, name, expanded } = table;
+  const { dbId, catalog, schema, name, expanded } = table;
   const theme = useTheme();
   const dispatch = useDispatch();
   const {
@@ -112,6 +112,7 @@ const TableElement = ({ table, ...props }: 
TableElementProps) => {
   } = useTableMetadataQuery(
     {
       dbId,
+      catalog,
       schema,
       table: name,
     },
@@ -125,6 +126,7 @@ const TableElement = ({ table, ...props }: 
TableElementProps) => {
   } = useTableExtendedMetadataQuery(
     {
       dbId,
+      catalog,
       schema,
       table: name,
     },
diff --git a/superset-frontend/src/SqlLab/fixtures.ts 
b/superset-frontend/src/SqlLab/fixtures.ts
index 742145c58c..54f1c278f8 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -36,6 +36,7 @@ export const table = {
   dbId: 1,
   selectStar: 'SELECT * FROM ab_user',
   queryEditorId: 'dfsadfs',
+  catalog: null,
   schema: 'superset',
   name: 'ab_user',
   id: 'r11Vgt60',
@@ -191,6 +192,7 @@ export const defaultQueryEditor = {
   selectedText: undefined,
   sql: 'SELECT *\nFROM\nWHERE',
   name: 'Untitled Query 1',
+  catalog: null,
   schema: 'main',
   remoteId: null,
   hideLeftBar: false,
@@ -233,6 +235,7 @@ export const queries = [
     queryLimit: 100,
     endDttm: 1476910566798,
     limit_reached: false,
+    catalog: null,
     schema: 'test_schema',
     errorMessage: null,
     db: 'main',
@@ -294,6 +297,7 @@ export const queries = [
     rows: 42,
     endDttm: 1476910579693,
     limit_reached: false,
+    catalog: null,
     schema: null,
     errorMessage: null,
     db: 'main',
@@ -323,6 +327,7 @@ export const queryWithNoQueryLimit = {
   rows: 42,
   endDttm: 1476910566798,
   limit_reached: false,
+  catalog: null,
   schema: 'test_schema',
   errorMessage: null,
   db: 'main',
@@ -456,18 +461,21 @@ export const tables = {
   options: [
     {
       value: 'birth_names',
+      catalog: null,
       schema: 'main',
       label: 'birth_names',
       title: 'birth_names',
     },
     {
       value: 'energy_usage',
+      catalog: null,
       schema: 'main',
       label: 'energy_usage',
       title: 'energy_usage',
     },
     {
       value: 'wb_health_population',
+      catalog: null,
       schema: 'main',
       label: 'wb_health_population',
       title: 'wb_health_population',
@@ -483,6 +491,7 @@ export const stoppedQuery = {
   progress: 0,
   results: [],
   runAsync: false,
+  catalog: null,
   schema: 'main',
   sql: 'SELECT ...',
   sqlEditorId: 'rJaf5u9WZ',
@@ -501,6 +510,7 @@ export const failedQueryWithErrorMessage = {
   progress: 0,
   results: [],
   runAsync: false,
+  catalog: null,
   schema: 'main',
   sql: 'SELECT ...',
   sqlEditorId: 'rJaf5u9WZ',
@@ -526,6 +536,7 @@ export const failedQueryWithErrors = {
   progress: 0,
   results: [],
   runAsync: false,
+  catalog: null,
   schema: 'main',
   sql: 'SELECT ...',
   sqlEditorId: 'rJaf5u9WZ',
@@ -555,6 +566,7 @@ const baseQuery: QueryResponse = {
   started: 'started',
   queryLimit: 100,
   endDttm: 1476910566798,
+  catalog: null,
   schema: 'test_schema',
   errorMessage: null,
   db: { key: 'main' },
@@ -689,6 +701,7 @@ export const query = {
   dbId: 1,
   sql: 'SELECT * FROM something',
   description: 'test description',
+  catalog: null,
   schema: 'test schema',
   resultsKey: 'test',
 };
@@ -698,6 +711,7 @@ export const queryId = 'clientId2353';
 export const testQuery: ISaveableDatasource = {
   name: 'unimportant',
   dbId: 1,
+  catalog: null,
   schema: 'main',
   sql: 'SELECT *',
   columns: [
@@ -727,6 +741,7 @@ export const mockdatasets = [...new Array(3)].map((_, i) => 
({
   database_name: `db ${i}`,
   explore_url: `/explore/?datasource_type=table&datasource_id=${i}`,
   id: i,
+  catalog: null,
   schema: `schema ${i}`,
   table_name: `coolest table ${i}`,
   owners: [{ username: 'admin', userId: 1 }],
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.ts 
b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
index bcdc1f40c3..52a9770854 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.ts
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts
@@ -89,6 +89,7 @@ export default function getInitialState({
         autorun: Boolean(activeTab.autorun),
         templateParams: activeTab.template_params || undefined,
         dbId: activeTab.database_id,
+        catalog: activeTab.catalog,
         schema: activeTab.schema,
         queryLimit: activeTab.query_limit,
         hideLeftBar: activeTab.hide_left_bar,
@@ -121,6 +122,7 @@ export default function getInitialState({
         const table = {
           dbId: tableSchema.database_id,
           queryEditorId: tableSchema.tab_state_id.toString(),
+          catalog: tableSchema.catalog,
           schema: tableSchema.schema,
           name: tableSchema.table,
           expanded: tableSchema.expanded,
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js 
b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index 9ffcb4dfcb..ecc0f090a9 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -109,6 +109,7 @@ export default function sqlLabReducer(state = {}, action) {
         remoteId: progenitor.remoteId,
         name: t('Copy of %s', progenitor.name),
         dbId: action.query.dbId ? action.query.dbId : null,
+        catalog: action.query.catalog ? action.query.catalog : null,
         schema: action.query.schema ? action.query.schema : null,
         autorun: true,
         sql: action.query.sql,
@@ -180,6 +181,7 @@ export default function sqlLabReducer(state = {}, action) {
         if (
           xt.dbId === at.dbId &&
           xt.queryEditorId === at.queryEditorId &&
+          xt.catalog === at.catalog &&
           xt.schema === at.schema &&
           xt.name === at.name
         ) {
@@ -503,6 +505,18 @@ export default function sqlLabReducer(state = {}, action) {
         ),
       };
     },
+    [actions.QUERY_EDITOR_SET_CATALOG]() {
+      return {
+        ...state,
+        ...alterUnsavedQueryEditorState(
+          state,
+          {
+            catalog: action.catalog,
+          },
+          action.queryEditor.id,
+        ),
+      };
+    },
     [actions.QUERY_EDITOR_SET_SCHEMA]() {
       return {
         ...state,
diff --git a/superset-frontend/src/SqlLab/types.ts 
b/superset-frontend/src/SqlLab/types.ts
index cac9ceb5d9..b1dea6f2e3 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -50,6 +50,7 @@ export interface QueryEditor {
   dbId?: number;
   name: string;
   title?: string; // keep it optional for backward compatibility
+  catalog?: string | null;
   schema?: string;
   autorun: boolean;
   sql: string;
@@ -81,6 +82,7 @@ export type UnsavedQueryEditor = Partial<QueryEditor>;
 export interface Table {
   id: string;
   dbId: number;
+  catalog: string | null;
   schema: string;
   name: string;
   queryEditorId: QueryEditor['id'];
diff --git 
a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts 
b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
index 8b7f41f9f7..3061de1f69 100644
--- a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
+++ b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts
@@ -109,22 +109,24 @@ export function rehydratePersistedState(
   state: SqlLabRootState,
 ) {
   // Rehydrate server side persisted table metadata
-  state.sqlLab.tables.forEach(({ name: table, schema, dbId, persistData }) => {
-    if (dbId && schema && table && persistData?.columns) {
-      dispatch(
-        tableApiUtil.upsertQueryData(
-          'tableMetadata',
-          { dbId, schema, table },
-          persistData,
-        ),
-      );
-      dispatch(
-        tableApiUtil.upsertQueryData(
-          'tableExtendedMetadata',
-          { dbId, schema, table },
-          {},
-        ),
-      );
-    }
-  });
+  state.sqlLab.tables.forEach(
+    ({ name: table, catalog, schema, dbId, persistData }) => {
+      if (dbId && schema && table && persistData?.columns) {
+        dispatch(
+          tableApiUtil.upsertQueryData(
+            'tableMetadata',
+            { dbId, catalog, schema, table },
+            persistData,
+          ),
+        );
+        dispatch(
+          tableApiUtil.upsertQueryData(
+            'tableExtendedMetadata',
+            { dbId, catalog, schema, table },
+            {},
+          ),
+        );
+      }
+    },
+  );
 }
diff --git 
a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx 
b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
index c3ad51cf60..32373301f6 100644
--- 
a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
+++ 
b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
@@ -40,6 +40,7 @@ const createProps = (): DatabaseSelectorProps => ({
   formMode: false,
   isDatabaseSelectEnabled: true,
   readOnly: false,
+  catalog: null,
   schema: 'public',
   sqlLabMode: true,
   getDbList: jest.fn(),
@@ -158,16 +159,23 @@ const fakeSchemaApiResult = {
   result: ['information_schema', 'public'],
 };
 
+const fakeCatalogApiResult = {
+  count: 0,
+  result: [],
+};
+
 const fakeFunctionNamesApiResult = {
   function_names: [],
 };
 
 const databaseApiRoute = 'glob:*/api/v1/database/?*';
+const catalogApiRoute = 'glob:*/api/v1/database/*/catalogs/?*';
 const schemaApiRoute = 'glob:*/api/v1/database/*/schemas/?*';
 const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*';
 
 function setupFetchMock() {
   fetchMock.get(databaseApiRoute, fakeDatabaseApiResult);
+  fetchMock.get(catalogApiRoute, fakeCatalogApiResult);
   fetchMock.get(schemaApiRoute, fakeSchemaApiResult);
   fetchMock.get(tablesApiRoute, fakeFunctionNamesApiResult);
 }
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx 
b/superset-frontend/src/components/DatabaseSelector/index.tsx
index 0c0268db5c..6eb1340d5b 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -24,7 +24,12 @@ import Label from 'src/components/Label';
 import { FormLabel } from 'src/components/Form';
 import RefreshLabel from 'src/components/RefreshLabel';
 import { useToasts } from 'src/components/MessageToasts/withToasts';
-import { useSchemas, SchemaOption } from 'src/hooks/apiResources';
+import {
+  useCatalogs,
+  CatalogOption,
+  useSchemas,
+  SchemaOption,
+} from 'src/hooks/apiResources';
 
 const DatabaseSelectorWrapper = styled.div`
   ${({ theme }) => `
@@ -81,6 +86,7 @@ export type DatabaseObject = {
   id: number;
   database_name: string;
   backend?: string;
+  allow_multi_catalog?: boolean;
 };
 
 export interface DatabaseSelectorProps {
@@ -92,9 +98,11 @@ export interface DatabaseSelectorProps {
   isDatabaseSelectEnabled?: boolean;
   onDbChange?: (db: DatabaseObject) => void;
   onEmptyResults?: (searchText?: string) => void;
+  onCatalogChange?: (catalog?: string) => void;
+  catalog?: string | null;
   onSchemaChange?: (schema?: string) => void;
-  readOnly?: boolean;
   schema?: string;
+  readOnly?: boolean;
   sqlLabMode?: boolean;
 }
 
@@ -113,6 +121,7 @@ const SelectLabel = ({
   </LabelStyle>
 );
 
+const EMPTY_CATALOG_OPTIONS: CatalogOption[] = [];
 const EMPTY_SCHEMA_OPTIONS: SchemaOption[] = [];
 
 export default function DatabaseSelector({
@@ -124,12 +133,20 @@ export default function DatabaseSelector({
   isDatabaseSelectEnabled = true,
   onDbChange,
   onEmptyResults,
+  onCatalogChange,
+  catalog,
   onSchemaChange,
-  readOnly = false,
   schema,
+  readOnly = false,
   sqlLabMode = false,
 }: DatabaseSelectorProps) {
+  const showCatalogSelector = !!db?.allow_multi_catalog;
   const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>();
+  const [currentCatalog, setCurrentCatalog] = useState<
+    CatalogOption | undefined
+  >(catalog ? { label: catalog, value: catalog, title: catalog } : undefined);
+  const catalogRef = useRef(catalog);
+  catalogRef.current = catalog;
   const [currentSchema, setCurrentSchema] = useState<SchemaOption | undefined>(
     schema ? { label: schema, value: schema, title: schema } : undefined,
   );
@@ -185,6 +202,7 @@ export default function DatabaseSelector({
             id: row.id,
             database_name: row.database_name,
             backend: row.backend,
+            allow_multi_catalog: row.allow_multi_catalog,
           }));
 
           return {
@@ -193,7 +211,7 @@ export default function DatabaseSelector({
           };
         });
       },
-    [formMode, getDbList, sqlLabMode],
+    [formMode, getDbList, sqlLabMode, onEmptyResults],
   );
 
   useEffect(() => {
@@ -223,11 +241,12 @@ export default function DatabaseSelector({
   }
 
   const {
-    data,
+    data: schemaData,
     isFetching: loadingSchemas,
-    refetch,
+    refetch: refetchSchemas,
   } = useSchemas({
     dbId: currentDb?.value,
+    catalog: currentCatalog?.value,
     onSuccess: (schemas, isFetched) => {
       if (schemas.length === 1) {
         changeSchema(schemas[0]);
@@ -244,17 +263,55 @@ export default function DatabaseSelector({
     onError: () => handleError(t('There was an error loading the schemas')),
   });
 
-  const schemaOptions = data || EMPTY_SCHEMA_OPTIONS;
+  const schemaOptions = schemaData || EMPTY_SCHEMA_OPTIONS;
+
+  function changeCatalog(catalog: CatalogOption | undefined) {
+    setCurrentCatalog(catalog);
+    setCurrentSchema(undefined);
+    if (onCatalogChange && catalog?.value !== catalogRef.current) {
+      onCatalogChange(catalog?.value);
+    }
+  }
+
+  const {
+    data: catalogData,
+    isFetching: loadingCatalogs,
+    refetch: refetchCatalogs,
+  } = useCatalogs({
+    dbId: currentDb?.value,
+    onSuccess: (catalogs, isFetched) => {
+      if (catalogs.length === 1) {
+        changeCatalog(catalogs[0]);
+      } else if (
+        !catalogs.find(
+          catalogOption => catalogRef.current === catalogOption.value,
+        )
+      ) {
+        changeCatalog(undefined);
+      }
+
+      if (isFetched) {
+        addSuccessToast('List refreshed');
+      }
+    },
+    onError: () => handleError(t('There was an error loading the catalogs')),
+  });
+
+  const catalogOptions = catalogData || EMPTY_CATALOG_OPTIONS;
 
-  function changeDataBase(
+  function changeDatabase(
     value: { label: string; value: number },
     database: DatabaseValue,
   ) {
     setCurrentDb(database);
+    setCurrentCatalog(undefined);
     setCurrentSchema(undefined);
     if (onDbChange) {
       onDbChange(database);
     }
+    if (onCatalogChange) {
+      onCatalogChange(undefined);
+    }
     if (onSchemaChange) {
       onSchemaChange(undefined);
     }
@@ -278,7 +335,7 @@ export default function DatabaseSelector({
         header={<FormLabel>{t('Database')}</FormLabel>}
         lazyLoading={false}
         notFoundContent={emptyState}
-        onChange={changeDataBase}
+        onChange={changeDatabase}
         value={currentDb}
         placeholder={t('Select database or type to search databases')}
         disabled={!isDatabaseSelectEnabled || readOnly}
@@ -288,10 +345,36 @@ export default function DatabaseSelector({
     );
   }
 
+  function renderCatalogSelect() {
+    const refreshIcon = !readOnly && (
+      <RefreshLabel
+        onClick={refetchCatalogs}
+        tooltipContent={t('Force refresh catalog list')}
+      />
+    );
+    return renderSelectRow(
+      <Select
+        ariaLabel={t('Select catalog or type to search catalogs')}
+        disabled={!currentDb || readOnly}
+        header={<FormLabel>{t('Catalog')}</FormLabel>}
+        labelInValue
+        loading={loadingCatalogs}
+        name="select-catalog"
+        notFoundContent={t('No compatible catalog found')}
+        placeholder={t('Select catalog or type to search catalogs')}
+        onChange={item => changeCatalog(item as CatalogOption)}
+        options={catalogOptions}
+        showSearch
+        value={currentCatalog}
+      />,
+      refreshIcon,
+    );
+  }
+
   function renderSchemaSelect() {
     const refreshIcon = !readOnly && (
       <RefreshLabel
-        onClick={() => refetch()}
+        onClick={refetchSchemas}
         tooltipContent={t('Force refresh schema list')}
       />
     );
@@ -317,6 +400,7 @@ export default function DatabaseSelector({
   return (
     <DatabaseSelectorWrapper data-test="DatabaseSelector">
       {renderDatabaseSelect()}
+      {showCatalogSelector && renderCatalogSelect()}
       {renderSchemaSelect()}
     </DatabaseSelectorWrapper>
   );
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx 
b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index b9af27d2af..1aa20312f7 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -758,6 +758,7 @@ class DatasourceEditor extends React.PureComponent {
       datasource_type: datasource.type || datasource.datasource_type,
       database_name:
         datasource.database.database_name || datasource.database.name,
+      catalog_name: datasource.catalog,
       schema_name: datasource.schema,
       table_name: datasource.table_name,
       normalize_columns: datasource.normalize_columns,
@@ -1090,7 +1091,12 @@ class DatasourceEditor extends React.PureComponent {
                         <div css={{ marginTop: 8 }}>
                           <DatabaseSelector
                             db={datasource?.database}
+                            catalog={datasource.catalog}
                             schema={datasource.schema}
+                            onCatalogChange={catalog =>
+                              this.state.isEditMode &&
+                              this.onDatasourcePropChange('catalog', catalog)
+                            }
                             onSchemaChange={schema =>
                               this.state.isEditMode &&
                               this.onDatasourcePropChange('schema', schema)
@@ -1164,9 +1170,16 @@ class DatasourceEditor extends React.PureComponent {
                         }}
                         dbId={datasource.database?.id}
                         handleError={this.props.addDangerToast}
+                        catalog={datasource.catalog}
                         schema={datasource.schema}
                         sqlLabMode={false}
                         tableValue={datasource.table_name}
+                        onCatalogChange={
+                          this.state.isEditMode
+                            ? catalog =>
+                                this.onDatasourcePropChange('catalog', catalog)
+                            : undefined
+                        }
                         onSchemaChange={
                           this.state.isEditMode
                             ? schema =>
diff --git 
a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx 
b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
index 13a2c33248..8f82818b8e 100644
--- a/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
+++ b/superset-frontend/src/components/TableSelector/TableSelector.test.tsx
@@ -54,6 +54,7 @@ const getTableMockFunction = () =>
   }) as any;
 
 const databaseApiRoute = 'glob:*/api/v1/database/?*';
+const catalogApiRoute = 'glob:*/api/v1/database/*/catalogs/?*';
 const schemaApiRoute = 'glob:*/api/v1/database/*/schemas/?*';
 const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*';
 
@@ -74,6 +75,7 @@ afterEach(() => {
 });
 
 test('renders with default props', async () => {
+  fetchMock.get(catalogApiRoute, { result: [] });
   fetchMock.get(schemaApiRoute, { result: [] });
   fetchMock.get(tablesApiRoute, getTableMockFunction());
 
@@ -96,6 +98,7 @@ test('renders with default props', async () => {
 });
 
 test('skips select all options', async () => {
+  fetchMock.get(catalogApiRoute, { result: [] });
   fetchMock.get(schemaApiRoute, { result: ['test_schema'] });
   fetchMock.get(tablesApiRoute, getTableMockFunction());
 
@@ -115,6 +118,7 @@ test('skips select all options', async () => {
 });
 
 test('renders table options without Select All option', async () => {
+  fetchMock.get(catalogApiRoute, { result: [] });
   fetchMock.get(schemaApiRoute, { result: ['test_schema'] });
   fetchMock.get(tablesApiRoute, getTableMockFunction());
 
@@ -133,6 +137,7 @@ test('renders table options without Select All option', 
async () => {
 });
 
 test('renders disabled without schema', async () => {
+  fetchMock.get(catalogApiRoute, { result: [] });
   fetchMock.get(schemaApiRoute, { result: [] });
   fetchMock.get(tablesApiRoute, getTableMockFunction());
 
@@ -150,6 +155,7 @@ test('renders disabled without schema', async () => {
 });
 
 test('table select retain value if not in SQL Lab mode', async () => {
+  fetchMock.get(catalogApiRoute, { result: [] });
   fetchMock.get(schemaApiRoute, { result: ['test_schema'] });
   fetchMock.get(tablesApiRoute, getTableMockFunction());
 
@@ -191,6 +197,7 @@ test('table select retain value if not in SQL Lab mode', 
async () => {
 });
 
 test('table multi select retain all the values selected', async () => {
+  fetchMock.get(catalogApiRoute, { result: [] });
   fetchMock.get(schemaApiRoute, { result: ['test_schema'] });
   fetchMock.get(tablesApiRoute, getTableMockFunction());
 
diff --git a/superset-frontend/src/components/TableSelector/index.tsx 
b/superset-frontend/src/components/TableSelector/index.tsx
index b7c92a978a..05e3808c49 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -97,13 +97,19 @@ interface TableSelectorProps {
   handleError: (msg: string) => void;
   isDatabaseSelectEnabled?: boolean;
   onDbChange?: (db: DatabaseObject) => void;
+  onCatalogChange?: (catalog?: string | null) => void;
   onSchemaChange?: (schema?: string) => void;
   readOnly?: boolean;
+  catalog?: string | null;
   schema?: string;
   onEmptyResults?: (searchText?: string) => void;
   sqlLabMode?: boolean;
   tableValue?: string | string[];
-  onTableSelectChange?: (value?: string | string[], schema?: string) => void;
+  onTableSelectChange?: (
+    value?: string | string[],
+    catalog?: string | null,
+    schema?: string,
+  ) => void;
   tableSelectMode?: 'single' | 'multiple';
   customTableOptionLabelRenderer?: (table: Table) => JSX.Element;
 }
@@ -159,9 +165,11 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
   handleError,
   isDatabaseSelectEnabled = true,
   onDbChange,
+  onCatalogChange,
   onSchemaChange,
   readOnly = false,
   onEmptyResults,
+  catalog,
   schema,
   sqlLabMode = true,
   tableSelectMode = 'single',
@@ -170,6 +178,9 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
   customTableOptionLabelRenderer,
 }) => {
   const { addSuccessToast } = useToasts();
+  const [currentCatalog, setCurrentCatalog] = useState<
+    string | null | undefined
+  >(catalog);
   const [currentSchema, setCurrentSchema] = useState<string | undefined>(
     schema,
   );
@@ -182,6 +193,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
     refetch,
   } = useTables({
     dbId: database?.id,
+    catalog: currentCatalog,
     schema: currentSchema,
     onSuccess: (data, isFetched) => {
       if (isFetched) {
@@ -218,6 +230,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
   useEffect(() => {
     // reset selections
     if (database === undefined) {
+      setCurrentCatalog(undefined);
       setCurrentSchema(undefined);
       setTableSelectValue(undefined);
     }
@@ -245,6 +258,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
         Array.isArray(selectedOptions)
           ? selectedOptions.map(option => option?.value)
           : selectedOptions?.value,
+        currentCatalog,
         currentSchema,
       );
     } else {
@@ -256,6 +270,22 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
     if (onDbChange) {
       onDbChange(db);
     }
+
+    setCurrentCatalog(undefined);
+    setCurrentSchema(undefined);
+    const value = tableSelectMode === 'single' ? undefined : [];
+    setTableSelectValue(value);
+  };
+
+  const internalCatalogChange = (catalog?: string | null) => {
+    setCurrentCatalog(catalog);
+    if (onCatalogChange) {
+      onCatalogChange(catalog);
+    }
+
+    setCurrentSchema(undefined);
+    const value = tableSelectMode === 'single' ? undefined : [];
+    setTableSelectValue(value);
   };
 
   const internalSchemaChange = (schema?: string) => {
@@ -265,7 +295,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
     }
 
     const value = tableSelectMode === 'single' ? undefined : [];
-    internalTableChange(value);
+    setTableSelectValue(value);
   };
 
   const handleFilterOption = useMemo(
@@ -328,6 +358,8 @@ const TableSelector: FunctionComponent<TableSelectorProps> 
= ({
         handleError={handleError}
         onDbChange={readOnly ? undefined : internalDbChange}
         onEmptyResults={onEmptyResults}
+        onCatalogChange={readOnly ? undefined : internalCatalogChange}
+        catalog={currentCatalog}
         onSchemaChange={readOnly ? undefined : internalSchemaChange}
         schema={currentSchema}
         sqlLabMode={sqlLabMode}
diff --git 
a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
 
b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
index 696861b289..55cd1c6071 100644
--- 
a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
+++ 
b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
@@ -48,6 +48,7 @@ import {
 } from 'src/explore/components/DatasourcePanel/types';
 import { DndItemType } from 'src/explore/components/DndItemType';
 import { ControlComponentProps } from 'src/explore/components/Control';
+import { toQueryString } from 'src/utils/urlUtils';
 import DndAdhocFilterOption from './DndAdhocFilterOption';
 import { useDefaultTimeFilter } from '../DateFilterControl/utils';
 import { Clauses, ExpressionTypes } from '../FilterControl/types';
@@ -175,13 +176,20 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
       const dbId = datasource.database?.id;
       const {
         datasource_name: name,
+        catalog,
         schema,
         is_sqllab_view: isSqllabView,
       } = datasource;
 
       if (!isSqllabView && dbId && name && schema) {
         SupersetClient.get({
-          endpoint: 
`/api/v1/database/${dbId}/table_metadata/extra/?name=${name}&schema=${schema}`,
+          endpoint: 
`/api/v1/database/${dbId}/table_metadata/extra/${toQueryString(
+            {
+              name,
+              catalog,
+              schema,
+            },
+          )}`,
         })
           .then(({ json }: { json: Record<string, any> }) => {
             if (json?.partitions) {
diff --git 
a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx
 
b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx
index c68a97b4d7..f4f604ecf0 100644
--- 
a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx
+++ 
b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx
@@ -48,6 +48,7 @@ import AdhocFilterOption from 
'src/explore/components/controls/FilterControl/Adh
 import AdhocFilter from 
'src/explore/components/controls/FilterControl/AdhocFilter';
 import adhocFilterType from 
'src/explore/components/controls/FilterControl/adhocFilterType';
 import columnType from 
'src/explore/components/controls/FilterControl/columnType';
+import { toQueryString } from 'src/utils/urlUtils';
 import { Clauses, ExpressionTypes } from '../types';
 
 const { warning } = Modal;
@@ -137,13 +138,20 @@ class AdhocFilterControl extends React.Component {
       const dbId = datasource.database?.id;
       const {
         datasource_name: name,
+        catalog,
         schema,
         is_sqllab_view: isSqllabView,
       } = datasource;
 
       if (!isSqllabView && dbId && name && schema) {
         SupersetClient.get({
-          endpoint: 
`/api/v1/database/${dbId}/table_metadata/extra/?name=${name}&schema=${schema}`,
+          endpoint: 
`/api/v1/database/${dbId}/table_metadata/extra/${toQueryString(
+            {
+              name,
+              catalog,
+              schema,
+            },
+          )}`,
         })
           .then(({ json }) => {
             if (json && json.partitions) {
diff --git a/superset-frontend/src/explore/types.ts 
b/superset-frontend/src/explore/types.ts
index 51b3013233..8ff0219fe6 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -66,6 +66,7 @@ export type OptionSortType = Partial<
 export type Datasource = Dataset & {
   database?: DatabaseObject;
   datasource?: string;
+  catalog?: string | null;
   schema?: string;
   is_sqllab_view?: boolean;
   extra?: string;
diff --git 
a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx 
b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
index 28e02a02f0..5700b59c85 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
@@ -56,6 +56,8 @@ const ExtraOptions = ({
   const createAsOpen = !!(db?.allow_ctas || db?.allow_cvas);
   const isFileUploadSupportedByEngine =
     db?.engine_information?.supports_file_upload;
+  const supportsDynamicCatalog =
+    db?.engine_information?.supports_dynamic_catalog;
 
   // JSON.parse will deep parse engine_params
   // if it's an object, and we want to keep it a string
@@ -191,7 +193,8 @@ const ExtraOptions = ({
                 <IndeterminateCheckbox
                   id="allows_virtual_table_explore"
                   indeterminate={false}
-                  checked={!!extraJson?.allows_virtual_table_explore}
+                  // when `allows_virtual_table_explore` is not present in 
`extra` it defaults to true
+                  checked={extraJson?.allows_virtual_table_explore !== false}
                   onChange={onExtraInputChange}
                   labelText={t('Allow this database to be explored')}
                 />
@@ -587,6 +590,24 @@ const ExtraOptions = ({
             />
           </div>
         </StyledInputContainer>
+        {supportsDynamicCatalog && (
+          <StyledInputContainer css={no_margin_bottom}>
+            <div className="input-container">
+              <IndeterminateCheckbox
+                id="allow_multi_catalog"
+                indeterminate={false}
+                checked={!!extraJson?.allow_multi_catalog}
+                onChange={onExtraInputChange}
+                labelText={t('Allow changing catalogs')}
+              />
+              <InfoTooltip
+                tooltip={t(
+                  'Give access to multiple catalogs in a single database 
connection.',
+                )}
+              />
+            </div>
+          </StyledInputContainer>
+        )}
       </Collapse.Panel>
     </Collapse>
   );
diff --git 
a/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.test.tsx
 
b/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.test.tsx
index fef205acf2..0ca40db0e9 100644
--- 
a/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.test.tsx
+++ 
b/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.test.tsx
@@ -96,6 +96,7 @@ test('Does not render if SSH Tunnel is disabled', () => {
         engine_information: {
           disable_ssh_tunneling: true,
           supports_file_upload: false,
+          supports_dynamic_catalog: false,
         },
       }}
     />,
diff --git 
a/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
 
b/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
index 44a8a60738..ce5efd09cb 100644
--- 
a/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
+++ 
b/superset-frontend/src/features/databases/UploadDataModel/UploadDataModal.test.tsx
@@ -47,6 +47,10 @@ fetchMock.get(
   },
 );
 
+fetchMock.get('glob:*api/v1/database/*/catalogs/', {
+  result: [],
+});
+
 fetchMock.get('glob:*api/v1/database/1/schemas/', {
   result: ['information_schema', 'public'],
 });
diff --git a/superset-frontend/src/features/databases/types.ts 
b/superset-frontend/src/features/databases/types.ts
index 06799ebafa..a09ad174a0 100644
--- a/superset-frontend/src/features/databases/types.ts
+++ b/superset-frontend/src/features/databases/types.ts
@@ -109,6 +109,7 @@ export type DatabaseObject = {
   engine_information?: {
     supports_file_upload?: boolean;
     disable_ssh_tunneling?: boolean;
+    supports_dynamic_catalog?: boolean;
   };
 
   // SSH Tunnel information
@@ -202,6 +203,7 @@ export type DatabaseForm = {
   engine_information: {
     supports_file_upload: boolean;
     disable_ssh_tunneling: boolean;
+    supports_dynamic_catalog: boolean;
   };
 };
 
@@ -223,6 +225,7 @@ export interface ExtraJson {
   cost_estimate_enabled?: boolean; // in SQL Lab
   disable_data_preview?: boolean; // in SQL Lab
   disable_drill_to_detail?: boolean;
+  allow_multi_catalog?: boolean;
   engine_params?: {
     catalog?: Record<string, string>;
     connect_args?: {
diff --git 
a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx 
b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx
index b3f8aec8f9..d4e5c98104 100644
--- a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/index.tsx
@@ -20,6 +20,7 @@ import React, { useEffect, useState, useRef } from 'react';
 import { SupersetClient, logging, t } from '@superset-ui/core';
 import { DatasetObject } from 'src/features/datasets/AddDataset/types';
 import { addDangerToast } from 'src/components/MessageToasts/actions';
+import { toQueryString } from 'src/utils/urlUtils';
 import DatasetPanel from './DatasetPanel';
 import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
 
@@ -51,8 +52,9 @@ export interface IDatasetPanelWrapperProps {
    */
   dbId?: number;
   /**
-   * The selected schema for the database
+   * The selected catalog/schema for the database
    */
+  catalog?: string | null;
   schema?: string | null;
   setHasColumns?: Function;
   datasets?: DatasetObject[] | undefined;
@@ -61,6 +63,7 @@ export interface IDatasetPanelWrapperProps {
 const DatasetPanelWrapper = ({
   tableName,
   dbId,
+  catalog,
   schema,
   setHasColumns,
   datasets,
@@ -74,9 +77,11 @@ const DatasetPanelWrapper = ({
     const { dbId, tableName, schema } = props;
     setLoading(true);
     setHasColumns?.(false);
-    const path = schema
-      ? 
`/api/v1/database/${dbId}/table_metadata/?name=${tableName}&schema=${schema}`
-      : `/api/v1/database/${dbId}/table_metadata/?name=${tableName}`;
+    const path = `/api/v1/database/${dbId}/table_metadata/${toQueryString({
+      name: tableName,
+      catalog,
+      schema,
+    })}`;
     try {
       const response = await SupersetClient.get({
         endpoint: path,
diff --git 
a/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx 
b/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx
index e0853cdc9d..c4a5df31c4 100644
--- a/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/Footer/index.tsx
@@ -90,6 +90,7 @@ function Footer({
     if (datasetObject) {
       const data = {
         database: datasetObject.db?.id,
+        catalog: datasetObject.catalog,
         schema: datasetObject.schema,
         table_name: datasetObject.table_name,
       };
diff --git 
a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx 
b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
index 093a65626b..d722f82b0f 100644
--- a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx
@@ -129,6 +129,14 @@ export default function LeftPanel({
     },
     [setDataset],
   );
+  const setCatalog = (catalog: string | null) => {
+    if (catalog) {
+      setDataset({
+        type: DatasetActionType.SelectCatalog,
+        payload: { name: 'catalog', value: catalog },
+      });
+    }
+  };
   const setSchema = (schema: string) => {
     if (schema) {
       setDataset({
@@ -178,10 +186,12 @@ export default function LeftPanel({
         handleError={addDangerToast}
         emptyState={emptyStateComponent(false)}
         onDbChange={setDatabase}
+        onCatalogChange={setCatalog}
         onSchemaChange={setSchema}
         onTableSelectChange={setTable}
         sqlLabMode={false}
         customTableOptionLabelRenderer={customTableOptionLabelRenderer}
+        {...(dataset?.catalog && { catalog: dataset.catalog })}
         {...(dataset?.schema && { schema: dataset.schema })}
       />
     </LeftPanelStyle>
diff --git a/superset-frontend/src/features/datasets/AddDataset/types.tsx 
b/superset-frontend/src/features/datasets/AddDataset/types.tsx
index 998d315940..1c4b16436d 100644
--- a/superset-frontend/src/features/datasets/AddDataset/types.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/types.tsx
@@ -20,6 +20,7 @@ import { DatabaseObject } from 
'src/components/DatabaseSelector';
 
 export enum DatasetActionType {
   SelectDatabase,
+  SelectCatalog,
   SelectSchema,
   SelectTable,
   ChangeDataset,
@@ -27,6 +28,7 @@ export enum DatasetActionType {
 
 export interface DatasetObject {
   db: DatabaseObject & { owners: [number] };
+  catalog?: string | null;
   schema?: string | null;
   dataset_name: string;
   table_name?: string | null;
@@ -50,6 +52,7 @@ export type DSReducerActionType =
   | {
       type:
         | DatasetActionType.ChangeDataset
+        | DatasetActionType.SelectCatalog
         | DatasetActionType.SelectSchema
         | DatasetActionType.SelectTable;
       payload: DatasetReducerPayloadType;
diff --git a/superset-frontend/src/hooks/apiResources/schemas.ts 
b/superset-frontend/src/hooks/apiResources/catalogs.ts
similarity index 72%
copy from superset-frontend/src/hooks/apiResources/schemas.ts
copy to superset-frontend/src/hooks/apiResources/catalogs.ts
index bbce48d5ad..1e6a97b344 100644
--- a/superset-frontend/src/hooks/apiResources/schemas.ts
+++ b/superset-frontend/src/hooks/apiResources/catalogs.ts
@@ -20,33 +20,32 @@ import { useCallback, useEffect, useRef } from 'react';
 import useEffectEvent from 'src/hooks/useEffectEvent';
 import { api, JsonResponse } from './queryApi';
 
-export type SchemaOption = {
+export type CatalogOption = {
   value: string;
   label: string;
   title: string;
 };
 
-export type FetchSchemasQueryParams = {
+export type FetchCatalogsQueryParams = {
   dbId?: string | number;
   forceRefresh: boolean;
-  onSuccess?: (data: SchemaOption[], isRefetched: boolean) => void;
+  onSuccess?: (data: CatalogOption[], isRefetched: boolean) => void;
   onError?: () => void;
 };
 
-type Params = Omit<FetchSchemasQueryParams, 'forceRefresh'>;
+type Params = Omit<FetchCatalogsQueryParams, 'forceRefresh'>;
 
-const schemaApi = api.injectEndpoints({
+const catalogApi = api.injectEndpoints({
   endpoints: builder => ({
-    schemas: builder.query<SchemaOption[], FetchSchemasQueryParams>({
-      providesTags: [{ type: 'Schemas', id: 'LIST' }],
+    catalogs: builder.query<CatalogOption[], FetchCatalogsQueryParams>({
+      providesTags: [{ type: 'Catalogs', id: 'LIST' }],
       query: ({ dbId, forceRefresh }) => ({
-        endpoint: `/api/v1/database/${dbId}/schemas/`,
-        // TODO: Would be nice to add pagination in a follow-up. Needs 
endpoint changes.
+        endpoint: `/api/v1/database/${dbId}/catalogs/`,
         urlParams: {
           force: forceRefresh,
         },
         transformResponse: ({ json }: JsonResponse) =>
-          json.result.map((value: string) => ({
+          json.result.sort().map((value: string) => ({
             value,
             label: value,
             title: value,
@@ -60,19 +59,19 @@ const schemaApi = api.injectEndpoints({
 });
 
 export const {
-  useLazySchemasQuery,
-  useSchemasQuery,
-  endpoints: schemaEndpoints,
-  util: schemaApiUtil,
-} = schemaApi;
+  useLazyCatalogsQuery,
+  useCatalogsQuery,
+  endpoints: catalogEndpoints,
+  util: catalogApiUtil,
+} = catalogApi;
 
-export const EMPTY_SCHEMAS = [] as SchemaOption[];
+export const EMPTY_CATALOGS = [] as CatalogOption[];
 
-export function useSchemas(options: Params) {
+export function useCatalogs(options: Params) {
   const isMountedRef = useRef(false);
   const { dbId, onSuccess, onError } = options || {};
-  const [trigger] = useLazySchemasQuery();
-  const result = useSchemasQuery(
+  const [trigger] = useLazyCatalogsQuery();
+  const result = useCatalogsQuery(
     { dbId, forceRefresh: false },
     {
       skip: !dbId,
@@ -80,7 +79,7 @@ export function useSchemas(options: Params) {
   );
 
   const handleOnSuccess = useEffectEvent(
-    (data: SchemaOption[], isRefetched: boolean) => {
+    (data: CatalogOption[], isRefetched: boolean) => {
       onSuccess?.(data, isRefetched);
     },
   );
@@ -94,7 +93,7 @@ export function useSchemas(options: Params) {
       trigger({ dbId, forceRefresh: true }).then(
         ({ isSuccess, isError, data }) => {
           if (isSuccess) {
-            handleOnSuccess(data || EMPTY_SCHEMAS, true);
+            handleOnSuccess(data || EMPTY_CATALOGS, true);
           }
           if (isError) {
             handleOnError();
@@ -110,7 +109,7 @@ export function useSchemas(options: Params) {
         result;
       if (!originalArgs?.forceRefresh && requestId && !isFetching) {
         if (isSuccess) {
-          handleOnSuccess(data || EMPTY_SCHEMAS, false);
+          handleOnSuccess(data || EMPTY_CATALOGS, false);
         }
         if (isError) {
           handleOnError();
diff --git a/superset-frontend/src/hooks/apiResources/index.ts 
b/superset-frontend/src/hooks/apiResources/index.ts
index faf4e5736b..53aa7aa113 100644
--- a/superset-frontend/src/hooks/apiResources/index.ts
+++ b/superset-frontend/src/hooks/apiResources/index.ts
@@ -26,6 +26,7 @@ export {
 // A central catalog of API Resource hooks.
 // Add new API hooks here, organized under
 // different files for different resource types.
+export * from './catalogs';
 export * from './charts';
 export * from './dashboards';
 export * from './tables';
diff --git a/superset-frontend/src/hooks/apiResources/queryApi.ts 
b/superset-frontend/src/hooks/apiResources/queryApi.ts
index 439418529f..b0c0bf5781 100644
--- a/superset-frontend/src/hooks/apiResources/queryApi.ts
+++ b/superset-frontend/src/hooks/apiResources/queryApi.ts
@@ -72,6 +72,7 @@ export const supersetClientQuery: BaseQueryFn<
 export const api = createApi({
   reducerPath: 'queryApi',
   tagTypes: [
+    'Catalogs',
     'Schemas',
     'Tables',
     'DatabaseFunctions',
diff --git a/superset-frontend/src/hooks/apiResources/queryValidations.ts 
b/superset-frontend/src/hooks/apiResources/queryValidations.ts
index 722c320049..df88d6afca 100644
--- a/superset-frontend/src/hooks/apiResources/queryValidations.ts
+++ b/superset-frontend/src/hooks/apiResources/queryValidations.ts
@@ -20,6 +20,7 @@ import { api, JsonResponse } from './queryApi';
 
 export type FetchValidationQueryParams = {
   dbId?: string | number;
+  catalog?: string | null;
   schema?: string;
   sql: string;
   templateParams?: string;
@@ -39,7 +40,7 @@ const queryValidationApi = api.injectEndpoints({
       FetchValidationQueryParams
     >({
       providesTags: ['QueryValidations'],
-      query: ({ dbId, schema, sql, templateParams }) => {
+      query: ({ dbId, catalog, schema, sql, templateParams }) => {
         let template_params = templateParams;
         try {
           template_params = JSON.parse(templateParams || '');
@@ -47,6 +48,7 @@ const queryValidationApi = api.injectEndpoints({
           template_params = undefined;
         }
         const postPayload = {
+          catalog,
           schema,
           sql,
           ...(template_params && { template_params }),
diff --git a/superset-frontend/src/hooks/apiResources/schemas.test.ts 
b/superset-frontend/src/hooks/apiResources/schemas.test.ts
index 70c1289d57..62e154acbe 100644
--- a/superset-frontend/src/hooks/apiResources/schemas.test.ts
+++ b/superset-frontend/src/hooks/apiResources/schemas.test.ts
@@ -80,7 +80,7 @@ describe('useSchemas hook', () => {
         })}`,
       ).length,
     ).toBe(1);
-    expect(onSuccess).toHaveBeenCalledTimes(1);
+    expect(onSuccess).toHaveBeenCalledTimes(2);
     act(() => {
       result.current.refetch();
     });
@@ -92,7 +92,7 @@ describe('useSchemas hook', () => {
         })}`,
       ).length,
     ).toBe(1);
-    expect(onSuccess).toHaveBeenCalledTimes(2);
+    expect(onSuccess).toHaveBeenCalledTimes(3);
     expect(result.current.data).toEqual(expectedResult);
   });
 
@@ -143,17 +143,17 @@ describe('useSchemas hook', () => {
 
     await waitFor(() => expect(result.current.data).toEqual(expectedResult));
     expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
-    expect(onSuccess).toHaveBeenCalledTimes(1);
+    expect(onSuccess).toHaveBeenCalledTimes(2);
 
     rerender({ dbId: 'db2' });
     await waitFor(() => expect(result.current.data).toEqual(expectedResult2));
     expect(fetchMock.calls(schemaApiRoute).length).toBe(2);
-    expect(onSuccess).toHaveBeenCalledTimes(2);
+    expect(onSuccess).toHaveBeenCalledTimes(4);
 
     rerender({ dbId: expectDbId });
     await waitFor(() => expect(result.current.data).toEqual(expectedResult));
     expect(fetchMock.calls(schemaApiRoute).length).toBe(2);
-    expect(onSuccess).toHaveBeenCalledTimes(3);
+    expect(onSuccess).toHaveBeenCalledTimes(5);
 
     // clean up cache
     act(() => {
diff --git a/superset-frontend/src/hooks/apiResources/schemas.ts 
b/superset-frontend/src/hooks/apiResources/schemas.ts
index bbce48d5ad..d5cec1daea 100644
--- a/superset-frontend/src/hooks/apiResources/schemas.ts
+++ b/superset-frontend/src/hooks/apiResources/schemas.ts
@@ -28,6 +28,7 @@ export type SchemaOption = {
 
 export type FetchSchemasQueryParams = {
   dbId?: string | number;
+  catalog?: string;
   forceRefresh: boolean;
   onSuccess?: (data: SchemaOption[], isRefetched: boolean) => void;
   onError?: () => void;
@@ -39,14 +40,15 @@ const schemaApi = api.injectEndpoints({
   endpoints: builder => ({
     schemas: builder.query<SchemaOption[], FetchSchemasQueryParams>({
       providesTags: [{ type: 'Schemas', id: 'LIST' }],
-      query: ({ dbId, forceRefresh }) => ({
+      query: ({ dbId, catalog, forceRefresh }) => ({
         endpoint: `/api/v1/database/${dbId}/schemas/`,
         // TODO: Would be nice to add pagination in a follow-up. Needs 
endpoint changes.
         urlParams: {
           force: forceRefresh,
+          ...(catalog !== undefined && { catalog }),
         },
         transformResponse: ({ json }: JsonResponse) =>
-          json.result.map((value: string) => ({
+          json.result.sort().map((value: string) => ({
             value,
             label: value,
             title: value,
@@ -70,10 +72,10 @@ export const EMPTY_SCHEMAS = [] as SchemaOption[];
 
 export function useSchemas(options: Params) {
   const isMountedRef = useRef(false);
-  const { dbId, onSuccess, onError } = options || {};
+  const { dbId, catalog, onSuccess, onError } = options || {};
   const [trigger] = useLazySchemasQuery();
   const result = useSchemasQuery(
-    { dbId, forceRefresh: false },
+    { dbId, catalog: catalog || undefined, forceRefresh: false },
     {
       skip: !dbId,
     },
@@ -89,9 +91,24 @@ export function useSchemas(options: Params) {
     onError?.();
   });
 
+  useEffect(() => {
+    if (dbId) {
+      trigger({ dbId, catalog, forceRefresh: false }).then(
+        ({ isSuccess, isError, data }) => {
+          if (isSuccess) {
+            handleOnSuccess(data || EMPTY_SCHEMAS, true);
+          }
+          if (isError) {
+            handleOnError();
+          }
+        },
+      );
+    }
+  }, [dbId, catalog, handleOnError, handleOnSuccess, trigger]);
+
   const refetch = useCallback(() => {
     if (dbId) {
-      trigger({ dbId, forceRefresh: true }).then(
+      trigger({ dbId, catalog, forceRefresh: true }).then(
         ({ isSuccess, isError, data }) => {
           if (isSuccess) {
             handleOnSuccess(data || EMPTY_SCHEMAS, true);
@@ -102,7 +119,7 @@ export function useSchemas(options: Params) {
         },
       );
     }
-  }, [dbId, handleOnError, handleOnSuccess, trigger]);
+  }, [dbId, catalog, handleOnError, handleOnSuccess, trigger]);
 
   useEffect(() => {
     if (isMountedRef.current) {
@@ -119,7 +136,7 @@ export function useSchemas(options: Params) {
     } else {
       isMountedRef.current = true;
     }
-  }, [result, handleOnSuccess, handleOnError]);
+  }, [catalog, result, handleOnSuccess, handleOnError]);
 
   return {
     ...result,
diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts 
b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
index 71e0cf2936..f25e9b4021 100644
--- a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
+++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.ts
@@ -33,6 +33,7 @@ const sqlEditorApi = api.injectEndpoints({
           version = LatestQueryEditorVersion,
           id,
           dbId,
+          catalog,
           schema,
           queryLimit,
           sql,
@@ -50,6 +51,7 @@ const sqlEditorApi = api.injectEndpoints({
         postPayload: pickBy(
           {
             database_id: dbId,
+            catalog,
             schema,
             sql,
             label: name,
diff --git a/superset-frontend/src/hooks/apiResources/sqlLab.ts 
b/superset-frontend/src/hooks/apiResources/sqlLab.ts
index 16e8ffde6c..45f7c83a38 100644
--- a/superset-frontend/src/hooks/apiResources/sqlLab.ts
+++ b/superset-frontend/src/hooks/apiResources/sqlLab.ts
@@ -27,6 +27,7 @@ export type InitialState = {
     label: string;
     active: boolean;
     database_id: number;
+    catalog?: string | null;
     schema?: string;
     table_schemas: {
       id: number;
@@ -38,6 +39,7 @@ export type InitialState = {
         }[];
         dataPreviewQueryId?: string;
       } & Record<string, any>;
+      catalog?: string | null;
       schema?: string;
       tab_state_id: number;
       database_id?: number;
diff --git a/superset-frontend/src/hooks/apiResources/tables.test.ts 
b/superset-frontend/src/hooks/apiResources/tables.test.ts
index e461b4f86b..7919370817 100644
--- a/superset-frontend/src/hooks/apiResources/tables.test.ts
+++ b/superset-frontend/src/hooks/apiResources/tables.test.ts
@@ -81,9 +81,11 @@ describe('useTables hook', () => {
   test('returns api response mapping json options', async () => {
     const expectDbId = 'db1';
     const expectedSchema = 'schema1';
+    const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
     const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`;
     const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`;
     fetchMock.get(tableApiRoute, fakeApiResult);
+    fetchMock.get(catalogApiRoute, { count: 0, result: [] });
     fetchMock.get(schemaApiRoute, {
       result: fakeSchemaApiResult,
     });
@@ -130,9 +132,11 @@ describe('useTables hook', () => {
   test('skips the deprecated schema option', async () => {
     const expectDbId = 'db1';
     const unexpectedSchema = 'invalid schema';
+    const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
     const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`;
     const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`;
     fetchMock.get(tableApiRoute, fakeApiResult);
+    fetchMock.get(catalogApiRoute, { count: 0, result: [] });
     fetchMock.get(schemaApiRoute, {
       result: fakeSchemaApiResult,
     });
@@ -166,6 +170,10 @@ describe('useTables hook', () => {
     const expectedSchema = 'schema2';
     const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`;
     fetchMock.get(tableApiRoute, fakeHasMoreApiResult);
+    fetchMock.get(`glob:*/api/v1/database/${expectDbId}/catalogs/*`, {
+      count: 0,
+      result: [],
+    });
     fetchMock.get(`glob:*/api/v1/database/${expectDbId}/schemas/*`, {
       result: fakeSchemaApiResult,
     });
@@ -191,6 +199,10 @@ describe('useTables hook', () => {
     const expectedSchema = 'schema1';
     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,
     });
@@ -220,6 +232,10 @@ describe('useTables hook', () => {
     fetchMock.get(tableApiRoute, url =>
       url.includes(expectedSchema) ? fakeApiResult : fakeHasMoreApiResult,
     );
+    fetchMock.get(`glob:*/api/v1/database/${expectDbId}/catalogs/*`, {
+      count: 0,
+      result: [],
+    });
     fetchMock.get(`glob:*/api/v1/database/${expectDbId}/schemas/*`, {
       result: fakeSchemaApiResult,
     });
diff --git a/superset-frontend/src/hooks/apiResources/tables.ts 
b/superset-frontend/src/hooks/apiResources/tables.ts
index 41be4c167c..d90c528b40 100644
--- a/superset-frontend/src/hooks/apiResources/tables.ts
+++ b/superset-frontend/src/hooks/apiResources/tables.ts
@@ -18,6 +18,7 @@
  */
 import { useCallback, useMemo, useEffect, useRef } from 'react';
 import useEffectEvent from 'src/hooks/useEffectEvent';
+import { toQueryString } from 'src/utils/urlUtils';
 import { api, JsonResponse } from './queryApi';
 
 import { useSchemas } from './schemas';
@@ -50,6 +51,7 @@ export type Data = {
 
 export type FetchTablesQueryParams = {
   dbId?: string | number;
+  catalog?: string | null;
   schema?: string;
   forceRefresh?: boolean;
   onSuccess?: (data: Data, isRefetched: boolean) => void;
@@ -58,6 +60,7 @@ export type FetchTablesQueryParams = {
 
 export type FetchTableMetadataQueryParams = {
   dbId: string | number;
+  catalog?: string | null;
   schema: string;
   table: string;
 };
@@ -95,12 +98,13 @@ const tableApi = api.injectEndpoints({
   endpoints: builder => ({
     tables: builder.query<Data, FetchTablesQueryParams>({
       providesTags: ['Tables'],
-      query: ({ dbId, schema, forceRefresh }) => ({
+      query: ({ dbId, catalog, schema, forceRefresh }) => ({
         endpoint: `/api/v1/database/${dbId ?? 'undefined'}/tables/`,
         // TODO: Would be nice to add pagination in a follow-up. Needs 
endpoint changes.
         urlParams: {
           force: forceRefresh,
           schema_name: schema ? encodeURIComponent(schema) : '',
+          ...(catalog && { catalog_name: catalog }),
         },
         transformResponse: ({ json }: QueryResponse) => ({
           options: json.result,
@@ -113,10 +117,12 @@ const tableApi = api.injectEndpoints({
       }),
     }),
     tableMetadata: builder.query<TableMetaData, 
FetchTableMetadataQueryParams>({
-      query: ({ dbId, schema, table }) => ({
-        endpoint: schema
-          ? 
`/api/v1/database/${dbId}/table_metadata/?name=${table}&schema=${schema}`
-          : `/api/v1/database/${dbId}/table_metadata/?name=${table}`,
+      query: ({ dbId, catalog, schema, table }) => ({
+        endpoint: `/api/v1/database/${dbId}/table_metadata/${toQueryString({
+          name: table,
+          catalog,
+          schema,
+        })}`,
         transformResponse: ({ json }: TableMetadataReponse) => json,
       }),
     }),
@@ -124,10 +130,10 @@ const tableApi = api.injectEndpoints({
       TableExtendedMetadata,
       FetchTableMetadataQueryParams
     >({
-      query: ({ dbId, schema, table }) => ({
-        endpoint: schema
-          ? 
`/api/v1/database/${dbId}/table_metadata/extra/?name=${table}&schema=${schema}`
-          : `/api/v1/database/${dbId}/table_metadata/extra/?name=${table}`,
+      query: ({ dbId, catalog, schema, table }) => ({
+        endpoint: 
`/api/v1/database/${dbId}/table_metadata/extra/${toQueryString(
+          { name: table, catalog, schema },
+        )}`,
         transformResponse: ({ json }: JsonResponse) => json,
       }),
     }),
@@ -144,22 +150,23 @@ export const {
 } = tableApi;
 
 export function useTables(options: Params) {
+  const { dbId, catalog, schema, onSuccess, onError } = options || {};
   const isMountedRef = useRef(false);
   const { data: schemaOptions, isFetching } = useSchemas({
-    dbId: options.dbId,
+    dbId,
+    catalog: catalog || undefined,
   });
   const schemaOptionsMap = useMemo(
     () => new Set(schemaOptions?.map(({ value }) => value)),
     [schemaOptions],
   );
-  const { dbId, schema, onSuccess, onError } = options || {};
 
   const enabled = Boolean(
     dbId && schema && !isFetching && schemaOptionsMap.has(schema),
   );
 
   const result = useTablesQuery(
-    { dbId, schema, forceRefresh: false },
+    { dbId, catalog, schema, forceRefresh: false },
     {
       skip: !enabled,
     },
@@ -176,7 +183,7 @@ export function useTables(options: Params) {
 
   const refetch = useCallback(() => {
     if (enabled) {
-      trigger({ dbId, schema, forceRefresh: true }).then(
+      trigger({ dbId, catalog, schema, forceRefresh: true }).then(
         ({ isSuccess, isError, data, error }) => {
           if (isSuccess && data) {
             handleOnSuccess(data, true);
@@ -187,7 +194,7 @@ export function useTables(options: Params) {
         },
       );
     }
-  }, [dbId, schema, enabled, handleOnSuccess, handleOnError, trigger]);
+  }, [dbId, catalog, schema, enabled, handleOnSuccess, handleOnError, 
trigger]);
 
   useEffect(() => {
     if (isMountedRef.current) {
diff --git a/superset-frontend/src/pages/DatasetCreation/index.tsx 
b/superset-frontend/src/pages/DatasetCreation/index.tsx
index 66acc4fa23..47378e828d 100644
--- a/superset-frontend/src/pages/DatasetCreation/index.tsx
+++ b/superset-frontend/src/pages/DatasetCreation/index.tsx
@@ -48,6 +48,14 @@ export function datasetReducer(
       return {
         ...trimmedState,
         ...action.payload,
+        catalog: null,
+        schema: null,
+        table_name: null,
+      };
+    case DatasetActionType.SelectCatalog:
+      return {
+        ...trimmedState,
+        [action.payload.name]: action.payload.value,
         schema: null,
         table_name: null,
       };
@@ -112,6 +120,7 @@ export default function AddDataset() {
     <DatasetPanel
       tableName={dataset?.table_name}
       dbId={dataset?.db?.id}
+      catalog={dataset?.catalog}
       schema={dataset?.schema}
       setHasColumns={setHasColumns}
       datasets={datasets}
diff --git a/superset-frontend/src/types/Database.ts 
b/superset-frontend/src/types/Database.ts
index 69e86e3a7b..a088fe26d3 100644
--- a/superset-frontend/src/types/Database.ts
+++ b/superset-frontend/src/types/Database.ts
@@ -29,4 +29,5 @@ export default interface Database {
   catalog: object;
   parameters: any;
   disable_drill_to_detail?: boolean;
+  allow_multi_catalog?: boolean;
 }
diff --git a/superset-frontend/src/utils/datasourceUtils.js 
b/superset-frontend/src/utils/datasourceUtils.js
index 1a5924b3e6..ef984044d1 100644
--- a/superset-frontend/src/utils/datasourceUtils.js
+++ b/superset-frontend/src/utils/datasourceUtils.js
@@ -21,5 +21,6 @@ export const getDatasourceAsSaveableDataset = source => ({
   name: source?.datasource_name || source?.name || 'Untitled',
   dbId: source?.database?.id || source?.dbId,
   sql: source?.sql || '',
+  catalog: source?.catalog,
   schema: source?.schema,
 });
diff --git a/superset-frontend/src/utils/urlUtils.test.ts 
b/superset-frontend/src/utils/urlUtils.test.ts
index 19e2a470d1..d992058821 100644
--- a/superset-frontend/src/utils/urlUtils.test.ts
+++ b/superset-frontend/src/utils/urlUtils.test.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { isUrlExternal, parseUrl } from './urlUtils';
+import { isUrlExternal, parseUrl, toQueryString } from './urlUtils';
 
 test('isUrlExternal', () => {
   expect(isUrlExternal('http://google.com')).toBeTruthy();
@@ -52,3 +52,47 @@ test('parseUrl', () => {
   expect(parseUrl('/about')).toEqual('/about');
   expect(parseUrl('#anchor')).toEqual('#anchor');
 });
+
+describe('toQueryString', () => {
+  it('should return an empty string if the input is an empty object', () => {
+    expect(toQueryString({})).toBe('');
+  });
+
+  it('should correctly convert a single key-value pair to a query string', () 
=> {
+    expect(toQueryString({ key: 'value' })).toBe('?key=value');
+  });
+
+  it('should correctly convert multiple key-value pairs to a query string', () 
=> {
+    expect(toQueryString({ key1: 'value1', key2: 'value2' })).toBe(
+      '?key1=value1&key2=value2',
+    );
+  });
+
+  it('should encode URI components', () => {
+    expect(
+      toQueryString({ 'a key': 'a value', email: '[email protected]' }),
+    ).toBe('?a%20key=a%20value&email=test%40example.com');
+  });
+
+  it('should omit keys with undefined values', () => {
+    expect(toQueryString({ key1: 'value1', key2: undefined })).toBe(
+      '?key1=value1',
+    );
+  });
+
+  it('should omit keys with null values', () => {
+    expect(toQueryString({ key1: 'value1', key2: null })).toBe('?key1=value1');
+  });
+
+  it('should handle numbers and boolean values as parameter values', () => {
+    expect(toQueryString({ number: 123, truth: true, lie: false })).toBe(
+      '?number=123&truth=true&lie=false',
+    );
+  });
+
+  it('should handle special characters in keys and values', () => {
+    expect(toQueryString({ 'user@domain': 'me&you' })).toBe(
+      '?user%40domain=me%26you',
+    );
+  });
+});
diff --git a/superset-frontend/src/utils/urlUtils.ts 
b/superset-frontend/src/utils/urlUtils.ts
index a44c3ed7a6..2858d65a7d 100644
--- a/superset-frontend/src/utils/urlUtils.ts
+++ b/superset-frontend/src/utils/urlUtils.ts
@@ -206,3 +206,16 @@ export function parseUrl(url: string) {
   }
   return url;
 }
+
+export function toQueryString(params: Record<string, any>): string {
+  const queryParts: string[] = [];
+  Object.keys(params).forEach(key => {
+    const value = params[key];
+    if (value !== null && value !== undefined) {
+      queryParts.push(
+        `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
+      );
+    }
+  });
+  return queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
+}
diff --git a/superset/cachekeys/api.py b/superset/cachekeys/api.py
index 3c90dafb72..91cae29b8d 100644
--- a/superset/cachekeys/api.py
+++ b/superset/cachekeys/api.py
@@ -85,6 +85,7 @@ class CacheRestApi(BaseSupersetModelRestApi):
         for ds in datasources.get("datasources", []):
             ds_obj = SqlaTable.get_datasource_by_name(
                 datasource_name=ds.get("datasource_name"),
+                catalog=ds.get("catalog"),
                 schema=ds.get("schema"),
                 database_name=ds.get("database_name"),
             )
diff --git a/superset/commands/dashboard/importers/v0.py 
b/superset/commands/dashboard/importers/v0.py
index 45089cbeac..48dcb230ef 100644
--- a/superset/commands/dashboard/importers/v0.py
+++ b/superset/commands/dashboard/importers/v0.py
@@ -66,6 +66,7 @@ def import_chart(
     datasource = SqlaTable.get_datasource_by_name(
         datasource_name=params["datasource_name"],
         database_name=params["database_name"],
+        catalog=params.get("catalog"),
         schema=params["schema"],
     )
     slc_to_import.datasource_id = datasource.id  # type: ignore
diff --git a/superset/commands/database/validate_sql.py 
b/superset/commands/database/validate_sql.py
index 6a93a01473..c13d4d5d33 100644
--- a/superset/commands/database/validate_sql.py
+++ b/superset/commands/database/validate_sql.py
@@ -60,8 +60,8 @@ class ValidateSQLCommand(BaseCommand):
         if not self._validator or not self._model:
             raise ValidatorSQLUnexpectedError()
         sql = self._properties["sql"]
-        schema = self._properties.get("schema")
         catalog = self._properties.get("catalog")
+        schema = self._properties.get("schema")
         try:
             timeout = current_app.config["SQLLAB_VALIDATION_TIMEOUT"]
             timeout_msg = f"The query exceeded the {timeout} seconds timeout."
diff --git a/superset/connectors/sqla/models.py 
b/superset/connectors/sqla/models.py
index 12fbdc3bd7..ca1adba0ac 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -698,7 +698,11 @@ class BaseDatasource(AuditMixinNullable, 
ImportExportMixin):  # pylint: disable=
 
     @classmethod
     def get_datasource_by_name(
-        cls, datasource_name: str, schema: str, database_name: str
+        cls,
+        datasource_name: str,
+        catalog: str | None,
+        schema: str,
+        database_name: str,
     ) -> BaseDatasource | None:
         raise NotImplementedError()
 
@@ -1239,6 +1243,7 @@ class SqlaTable(
     def get_datasource_by_name(
         cls,
         datasource_name: str,
+        catalog: str | None,
         schema: str | None,
         database_name: str,
     ) -> SqlaTable | None:
@@ -1248,6 +1253,7 @@ class SqlaTable(
             .join(Database)
             .filter(cls.table_name == datasource_name)
             .filter(Database.database_name == database_name)
+            .filter(cls.catalog == catalog)
         )
         # Handling schema being '' or None, which is easier to handle
         # in python than in the SQLA query in a multi-dialect way
@@ -1752,7 +1758,7 @@ class SqlaTable(
         try:
             df = self.database.get_df(
                 sql,
-                None,
+                self.catalog,
                 self.schema or None,
                 mutator=assign_column_label,
             )
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 83c533b86e..29b13ae7b8 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -216,6 +216,7 @@ class DatabaseSchema(Schema):
     allows_virtual_table_explore = fields.Bool()
     disable_data_preview = fields.Bool()
     disable_drill_to_detail = fields.Bool()
+    allow_multi_catalog = fields.Bool()
     explore_database_id = fields.Int()
 
 
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 116ac9f46b..31db3ceacc 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -217,6 +217,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         "uuid",
         "disable_data_preview",
         "disable_drill_to_detail",
+        "allow_multi_catalog",
         "engine_information",
     ]
     add_columns = [
diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py
index 4318a1b48c..e6f6eaa12e 100644
--- a/superset/databases/schemas.py
+++ b/superset/databases/schemas.py
@@ -156,7 +156,9 @@ extra_description = markdown(
     "6. The ``disable_data_preview`` field is a boolean specifying whether or 
not data "
     "preview queries will be run when fetching table metadata in SQL Lab."
     "7. The ``disable_drill_to_detail`` field is a boolean specifying whether 
or not"
-    "drill to detail is disabled for the database.",
+    "drill to detail is disabled for the database."
+    "8. The ``allow_multi_catalog`` indicates if the database allows changing "
+    "the default catalog when running queries and creating datasets.",
     True,
 )
 get_export_ids_schema = {"type": "array", "items": {"type": "integer"}}
@@ -739,6 +741,7 @@ class ValidateSQLRequest(Schema):
     sql = fields.String(
         required=True, metadata={"description": "SQL statement to validate"}
     )
+    catalog = fields.String(required=False, allow_none=True)
     schema = fields.String(required=False, allow_none=True)
     template_params = fields.Dict(required=False, allow_none=True)
 
@@ -824,6 +827,7 @@ class ImportV1DatabaseExtraSchema(Schema):
     cancel_query_on_windows_unload = fields.Boolean(required=False)
     disable_data_preview = fields.Boolean(required=False)
     disable_drill_to_detail = fields.Boolean(required=False)
+    allow_multi_catalog = fields.Boolean(required=False)
     version = fields.String(required=False, allow_none=True)
 
 
@@ -968,6 +972,20 @@ class DatabaseSchemaAccessForFileUploadResponse(Schema):
     )
 
 
+class EngineInformationSchema(Schema):
+    supports_file_upload = fields.Boolean(
+        metadata={"description": "Users can upload files to the database"}
+    )
+    disable_ssh_tunneling = fields.Boolean(
+        metadata={"description": "SSH tunnel is not available to the database"}
+    )
+    supports_dynamic_catalog = fields.Boolean(
+        metadata={
+            "description": "The database supports multiple catalogs in a 
single connection"
+        }
+    )
+
+
 class DatabaseConnectionSchema(Schema):
     """
     Schema with database connection information.
@@ -1001,7 +1019,7 @@ class DatabaseConnectionSchema(Schema):
     driver = fields.String(
         allow_none=True, metadata={"description": "SQLAlchemy driver to use"}
     )
-    engine_information = fields.Dict(keys=fields.String(), values=fields.Raw())
+    engine_information = fields.Nested(EngineInformationSchema)
     expose_in_sqllab = fields.Boolean(
         metadata={"description": expose_in_sqllab_description}
     )
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index f6dedc97eb..0b793392d7 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -119,6 +119,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "owners.id",
         "owners.first_name",
         "owners.last_name",
+        "catalog",
         "schema",
         "sql",
         "table_name",
@@ -126,6 +127,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
     list_select_columns = list_columns + ["changed_on", "changed_by_fk"]
     order_columns = [
         "table_name",
+        "catalog",
         "schema",
         "changed_by.first_name",
         "changed_on_delta_humanized",
@@ -139,6 +141,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "sql",
         "filter_select_enabled",
         "fetch_values_predicate",
+        "catalog",
         "schema",
         "description",
         "main_dttm_col",
@@ -197,6 +200,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
     show_columns = show_select_columns + [
         "columns.type_generic",
         "database.backend",
+        "database.allow_multi_catalog",
         "columns.advanced_data_type",
         "is_managed_externally",
         "uid",
@@ -212,12 +216,13 @@ class DatasetRestApi(BaseSupersetModelRestApi):
     add_model_schema = DatasetPostSchema()
     edit_model_schema = DatasetPutSchema()
     duplicate_model_schema = DatasetDuplicateSchema()
-    add_columns = ["database", "schema", "table_name", "sql", "owners"]
+    add_columns = ["database", "catalog", "schema", "table_name", "sql", 
"owners"]
     edit_columns = [
         "table_name",
         "sql",
         "filter_select_enabled",
         "fetch_values_predicate",
+        "catalog",
         "schema",
         "description",
         "main_dttm_col",
@@ -251,6 +256,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "id",
         "database",
         "owners",
+        "catalog",
         "schema",
         "sql",
         "table_name",
@@ -258,7 +264,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
         "changed_by",
     ]
     allowed_rel_fields = {"database", "owners", "created_by", "changed_by"}
-    allowed_distinct_fields = {"schema"}
+    allowed_distinct_fields = {"catalog", "schema"}
 
     apispec_parameter_schemas = {
         "get_export_ids_schema": get_export_ids_schema,
diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py
index 45a44043a5..5ce0621675 100644
--- a/superset/datasets/schemas.py
+++ b/superset/datasets/schemas.py
@@ -93,6 +93,7 @@ class DatasetMetricsPutSchema(Schema):
 
 class DatasetPostSchema(Schema):
     database = fields.Integer(required=True)
+    catalog = fields.String(allow_none=True, validate=Length(0, 250))
     schema = fields.String(allow_none=True, validate=Length(0, 250))
     table_name = fields.String(required=True, allow_none=False, 
validate=Length(1, 250))
     sql = fields.String(allow_none=True)
@@ -109,6 +110,7 @@ class DatasetPutSchema(Schema):
     sql = fields.String(allow_none=True)
     filter_select_enabled = fields.Boolean(allow_none=True)
     fetch_values_predicate = fields.String(allow_none=True, validate=Length(0, 
1000))
+    catalog = fields.String(allow_none=True, validate=Length(0, 250))
     schema = fields.String(allow_none=True, validate=Length(0, 255))
     description = fields.String(allow_none=True)
     main_dttm_col = fields.String(allow_none=True)
@@ -272,6 +274,11 @@ class GetOrCreateDatasetSchema(Schema):
     database_id = fields.Integer(
         required=True, metadata={"description": "ID of database table belongs 
to"}
     )
+    catalog = fields.String(
+        allow_none=True,
+        validate=Length(0, 250),
+        metadata={"description": "The catalog the table belongs to"},
+    )
     schema = fields.String(
         allow_none=True,
         validate=Length(0, 250),
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index 5b8a1deaba..bcee08708d 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -2184,6 +2184,7 @@ class BaseEngineSpec:  # pylint: 
disable=too-many-public-methods
         return {
             "supports_file_upload": cls.supports_file_upload,
             "disable_ssh_tunneling": cls.disable_ssh_tunneling,
+            "supports_dynamic_catalog": cls.supports_dynamic_catalog,
         }
 
     @classmethod
diff --git a/superset/models/core.py b/superset/models/core.py
index fe486bf2b1..bbf3052342 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -235,6 +235,10 @@ class Database(Model, AuditMixinNullable, 
ImportExportMixin):  # pylint: disable
         # this will prevent any 'trash value' strings from going through
         return self.get_extra().get("disable_drill_to_detail", False) is True
 
+    @property
+    def allow_multi_catalog(self) -> bool:
+        return self.get_extra().get("allow_multi_catalog", False)
+
     @property
     def schema_options(self) -> dict[str, Any]:
         """Additional schema display config for engines with complex schemas"""
@@ -255,6 +259,7 @@ class Database(Model, AuditMixinNullable, 
ImportExportMixin):  # pylint: disable
             "parameters": self.parameters,
             "disable_data_preview": self.disable_data_preview,
             "disable_drill_to_detail": self.disable_drill_to_detail,
+            "allow_multi_catalog": self.allow_multi_catalog,
             "parameters_schema": self.parameters_schema,
             "engine_information": self.engine_information,
         }
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index 31443b4bb1..4c06867501 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -514,6 +514,7 @@ class TabState(AuditMixinNullable, ExtraJSONMixin, Model):
             "label": self.label,
             "active": self.active,
             "database_id": self.database_id,
+            "catalog": self.catalog,
             "schema": self.schema,
             "table_schemas": [ts.to_dict() for ts in self.table_schemas],
             "sql": self.sql,
diff --git a/superset/queries/saved_queries/api.py 
b/superset/queries/saved_queries/api.py
index be9a3f00b3..d772483d9c 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -95,6 +95,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
         "description",
         "id",
         "label",
+        "catalog",
         "schema",
         "sql",
         "sql_tables",
@@ -119,6 +120,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
         "label",
         "last_run_delta_humanized",
         "rows",
+        "catalog",
         "schema",
         "sql",
         "sql_tables",
@@ -130,12 +132,14 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
         "db_id",
         "description",
         "label",
+        "catalog",
         "schema",
         "sql",
         "template_parameters",
     ]
     edit_columns = add_columns
     order_columns = [
+        "catalog",
         "schema",
         "label",
         "description",
@@ -148,7 +152,15 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
         "last_run_delta_humanized",
     ]
 
-    search_columns = ["id", "database", "label", "schema", "created_by", 
"changed_by"]
+    search_columns = [
+        "id",
+        "database",
+        "label",
+        "catalog",
+        "schema",
+        "created_by",
+        "changed_by",
+    ]
     if is_feature_enabled("TAGGING_SYSTEM"):
         search_columns += ["tags"]
     search_filters = {
@@ -170,7 +182,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
     }
     base_related_field_filters = {"database": [["id", DatabaseFilter, lambda: 
[]]]}
     allowed_rel_fields = {"database", "changed_by", "created_by"}
-    allowed_distinct_fields = {"schema"}
+    allowed_distinct_fields = {"catalog", "schema"}
 
     def pre_add(self, item: SavedQuery) -> None:
         item.user = g.user
diff --git a/superset/sqllab/schemas.py b/superset/sqllab/schemas.py
index 0864420c90..5e22a97e2a 100644
--- a/superset/sqllab/schemas.py
+++ b/superset/sqllab/schemas.py
@@ -53,6 +53,7 @@ class ExecutePayloadSchema(Schema):
     client_id = fields.String(allow_none=True)
     queryLimit = fields.Integer(allow_none=True)
     sql_editor_id = fields.String(allow_none=True)
+    catalog = fields.String(allow_none=True)
     schema = fields.String(allow_none=True)
     tab = fields.String(allow_none=True)
     ctas_method = fields.String(allow_none=True)
diff --git a/superset/sqllab/sqllab_execution_context.py 
b/superset/sqllab/sqllab_execution_context.py
index 7a732cf642..7ab4459be3 100644
--- a/superset/sqllab/sqllab_execution_context.py
+++ b/superset/sqllab/sqllab_execution_context.py
@@ -44,6 +44,7 @@ SqlResults = dict[str, Any]
 @dataclass
 class SqlJsonExecutionContext:  # pylint: disable=too-many-instance-attributes
     database_id: int
+    catalog: str | None
     schema: str
     sql: str
     template_params: dict[str, Any]
@@ -73,6 +74,7 @@ class SqlJsonExecutionContext:  # pylint: 
disable=too-many-instance-attributes
 
     def _init_from_query_params(self, query_params: dict[str, Any]) -> None:
         self.database_id = cast(int, query_params.get("database_id"))
+        self.catalog = cast(str, query_params.get("catalog"))
         self.schema = cast(str, query_params.get("schema"))
         self.sql = cast(str, query_params.get("sql"))
         self.template_params = self._get_template_params(query_params)
@@ -147,6 +149,7 @@ class SqlJsonExecutionContext:  # pylint: 
disable=too-many-instance-attributes
             return Query(
                 database_id=self.database_id,
                 sql=self.sql,
+                catalog=self.catalog,
                 schema=self.schema,
                 select_as_cta=True,
                 ctas_method=self.create_table_as_select.ctas_method,  # type: 
ignore
@@ -163,6 +166,7 @@ class SqlJsonExecutionContext:  # pylint: 
disable=too-many-instance-attributes
         return Query(
             database_id=self.database_id,
             sql=self.sql,
+            catalog=self.catalog,
             schema=self.schema,
             select_as_cta=False,
             start_time=start_time,
diff --git a/superset/sqllab/utils.py b/superset/sqllab/utils.py
index bbf3919640..65b87bbf6e 100644
--- a/superset/sqllab/utils.py
+++ b/superset/sqllab/utils.py
@@ -39,6 +39,7 @@ DATABASE_KEYS = [
     "id",
     "disable_data_preview",
     "disable_drill_to_detail",
+    "allow_multi_catalog",
 ]
 
 
diff --git a/superset/views/database/mixins.py 
b/superset/views/database/mixins.py
index 0d104aad5f..21c664fa1f 100644
--- a/superset/views/database/mixins.py
+++ b/superset/views/database/mixins.py
@@ -149,7 +149,9 @@ class DatabaseMixin:
             "not data preview queries will be run when fetching table metadata 
in"
             "SQL Lab."
             "7. The ``disable_drill_to_detail`` field is a boolean specifying 
whether or"
-            "not drill to detail is disabled for the database.",
+            "not drill to detail is disabled for the database."
+            "8. The ``allow_multi_catalog`` indicates if the database allows 
changing "
+            "the default catalog when running queries and creating datasets.",
             True,
         ),
         "encrypted_extra": utils.markdown(
diff --git a/superset/views/datasource/schemas.py 
b/superset/views/datasource/schemas.py
index 73496b02b2..8ae28f9c7a 100644
--- a/superset/views/datasource/schemas.py
+++ b/superset/views/datasource/schemas.py
@@ -26,6 +26,7 @@ from superset.utils.core import DatasourceType
 class ExternalMetadataParams(TypedDict):
     datasource_type: str
     database_name: str
+    catalog_name: Optional[str]
     schema_name: str
     table_name: str
     normalize_columns: Optional[bool]
@@ -45,6 +46,7 @@ get_external_metadata_schema = {
 class ExternalMetadataSchema(Schema):
     datasource_type = fields.Str(required=True)
     database_name = fields.Str(required=True)
+    catalog_name = fields.Str(allow_none=True)
     schema_name = fields.Str(allow_none=True)
     table_name = fields.Str(required=True)
     normalize_columns = fields.Bool(allow_none=True)
@@ -60,6 +62,7 @@ class ExternalMetadataSchema(Schema):
         return ExternalMetadataParams(
             datasource_type=data["datasource_type"],
             database_name=data["database_name"],
+            catalog_name=data.get("catalog_name"),
             schema_name=data.get("schema_name", ""),
             table_name=data["table_name"],
             normalize_columns=data["normalize_columns"],
diff --git a/superset/views/datasource/views.py 
b/superset/views/datasource/views.py
index 7f81081777..27371a5d2e 100644
--- a/superset/views/datasource/views.py
+++ b/superset/views/datasource/views.py
@@ -165,6 +165,7 @@ class Datasource(BaseSupersetView):
 
         datasource = SqlaTable.get_datasource_by_name(
             database_name=params["database_name"],
+            catalog=params.get("catalog_name"),
             schema=params["schema_name"],
             datasource_name=params["table_name"],
         )
diff --git a/superset/views/sql_lab/views.py b/superset/views/sql_lab/views.py
index 4ed5143bb6..6e2738ea2e 100644
--- a/superset/views/sql_lab/views.py
+++ b/superset/views/sql_lab/views.py
@@ -91,6 +91,7 @@ class TabStateView(BaseSupersetView):
             or query_editor.get("title", __("Untitled Query")),
             active=True,
             database_id=query_editor["dbId"],
+            catalog=query_editor.get("catalog"),
             schema=query_editor.get("schema"),
             sql=query_editor.get("sql", "SELECT ..."),
             query_limit=query_editor.get("queryLimit"),

Reply via email to