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

beto pushed a commit to branch default-schema-catalog
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 4e52bc8a0bb496d1f9f5212e12275225774fa743
Author: Beto Dealmeida <robe...@dealmeida.net>
AuthorDate: Wed Sep 3 15:57:01 2025 -0400

    Update frontend
---
 .../AceEditorWrapper/useKeywords.test.ts           |  26 ++--
 .../components/AceEditorWrapper/useKeywords.ts     |   2 +-
 .../DatabaseSelector/DatabaseSelector.test.tsx     | 142 +++++++++++++++++++++
 .../src/components/DatabaseSelector/index.tsx      | 110 ++++++++++++----
 .../src/hooks/apiResources/catalogs.ts             |  25 +++-
 .../src/hooks/apiResources/schemas.test.ts         |  42 +++---
 .../src/hooks/apiResources/schemas.ts              |  25 +++-
 superset-frontend/src/hooks/apiResources/tables.ts |   2 +-
 8 files changed, 312 insertions(+), 62 deletions(-)

diff --git 
a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
index e0f49b70e3..f1ced5c34c 100644
--- 
a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
+++ 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
@@ -73,11 +73,14 @@ beforeEach(() => {
           dbId: expectDbId,
           forceRefresh: false,
         },
-        fakeSchemaApiResult.map(value => ({
-          value,
-          label: value,
-          title: value,
-        })),
+        {
+          result: fakeSchemaApiResult.map(value => ({
+            value,
+            label: value,
+            title: value,
+          })),
+          default: null,
+        },
       ),
     );
     store.dispatch(
@@ -307,11 +310,14 @@ test('returns long keywords with docText', async () => {
           dbId: expectLongKeywordDbId,
           forceRefresh: false,
         },
-        ['short', longKeyword].map(value => ({
-          value,
-          label: value,
-          title: value,
-        })),
+        {
+          result: ['short', longKeyword].map(value => ({
+            value,
+            label: value,
+            title: value,
+          })),
+          default: null,
+        },
       ),
     );
   });
diff --git 
a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts 
b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
index 99e9912513..d5f96599aa 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
@@ -164,7 +164,7 @@ export function useKeywords(
 
   const schemaKeywords = useMemo(
     () =>
-      (schemaOptions ?? []).map(s => ({
+      (schemaOptions?.result ?? []).map(s => ({
         name: s.label,
         value: s.value,
         score: SCHEMA_AUTOCOMPLETE_SCORE,
diff --git 
a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx 
b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
index f8d0425c5e..fd5c0b43c5 100644
--- 
a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
+++ 
b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx
@@ -163,11 +163,13 @@ const fakeDatabaseApiResultInReverseOrder = {
 const fakeSchemaApiResult = {
   count: 2,
   result: ['information_schema', 'public'],
+  default: 'public',
 };
 
 const fakeCatalogApiResult = {
   count: 0,
   result: [],
+  default: null,
 };
 
 const fakeFunctionNamesApiResult = {
@@ -382,3 +384,143 @@ test('Sends the correct schema when changing the schema', 
async () => {
   );
   expect(props.onSchemaChange).toHaveBeenCalledTimes(1);
 });
+
+test('Should auto-select default schema on load', async () => {
+  const props = createProps();
+  // Remove initial schema to test auto-selection
+  const propsWithoutSchema = { ...props, schema: undefined };
+
+  render(<DatabaseSelector {...propsWithoutSchema} />, {
+    useRedux: true,
+    store,
+  });
+
+  // Wait for the default schema to be auto-selected
+  await waitFor(() => {
+    expect(props.onSchemaChange).toHaveBeenCalledWith('public');
+  });
+});
+
+test('Should auto-select default catalog for multi-catalog databases', async 
() => {
+  const multiCatalogApiResult = {
+    count: 2,
+    result: ['catalog1', 'catalog2'],
+    default: 'catalog1',
+  };
+
+  fetchMock.get(catalogApiRoute, multiCatalogApiResult, {
+    overwriteRoutes: true,
+  });
+
+  const props = createProps();
+  const multiCatalogDb = {
+    ...props.db!,
+    allow_multi_catalog: true,
+  };
+  const multiCatalogProps = {
+    ...props,
+    db: multiCatalogDb,
+    catalog: undefined,
+    onCatalogChange: jest.fn(),
+  };
+
+  render(<DatabaseSelector {...multiCatalogProps} />, {
+    useRedux: true,
+    store,
+  });
+
+  // Wait for the default catalog to be auto-selected
+  await waitFor(() => {
+    expect(multiCatalogProps.onCatalogChange).toHaveBeenCalledWith('catalog1');
+  });
+});
+
+test('Should disable schema dropdown while catalogs are loading', async () => {
+  const props = createProps();
+  const multiCatalogDb = {
+    ...props.db!,
+    allow_multi_catalog: true,
+  };
+  const multiCatalogProps = {
+    ...props,
+    db: multiCatalogDb,
+    catalog: undefined,
+  };
+
+  // Mock a delayed catalog response
+  fetchMock.get(
+    catalogApiRoute,
+    new Promise(resolve =>
+      setTimeout(
+        () =>
+          resolve({
+            count: 1,
+            result: ['default_catalog'],
+            default: 'default_catalog',
+          }),
+        100,
+      ),
+    ),
+    { overwriteRoutes: true },
+  );
+
+  render(<DatabaseSelector {...multiCatalogProps} />, {
+    useRedux: true,
+    store,
+  });
+
+  // Initially, schema dropdown should be disabled while catalogs load
+  const schemaSelect = screen.getByRole('combobox', {
+    name: /select schema or type to search schemas/i,
+  });
+
+  expect(schemaSelect).toBeDisabled();
+
+  // Wait for catalogs to load and schema dropdown to be enabled
+  await waitFor(
+    () => {
+      expect(schemaSelect).toBeEnabled();
+    },
+    { timeout: 200 },
+  );
+});
+
+test('Should not fetch schemas until catalog is selected for multi-catalog 
databases', async () => {
+  const props = createProps();
+  const multiCatalogDb = {
+    ...props.db!,
+    allow_multi_catalog: true,
+  };
+  const multiCatalogProps = {
+    ...props,
+    db: multiCatalogDb,
+    catalog: undefined,
+  };
+
+  render(<DatabaseSelector {...multiCatalogProps} />, {
+    useRedux: true,
+    store,
+  });
+
+  // Initially, schemas should not be fetched
+  expect(fetchMock.calls(schemaApiRoute).length).toBe(0);
+
+  // Wait a bit to ensure schemas are still not fetched
+  await new Promise(resolve => setTimeout(resolve, 100));
+  expect(fetchMock.calls(schemaApiRoute).length).toBe(0);
+});
+
+test('Should fetch schemas immediately for non-catalog databases', async () => 
{
+  const props = createProps();
+  // Non-catalog database (allow_multi_catalog is not set)
+
+  render(<DatabaseSelector {...props} />, {
+    useRedux: true,
+    store,
+  });
+
+  // Schemas should be fetched immediately for non-catalog databases
+  await waitFor(() => {
+    expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
+  });
+});
diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx 
b/superset-frontend/src/components/DatabaseSelector/index.tsx
index c50abd4442..eb8d38813f 100644
--- a/superset-frontend/src/components/DatabaseSelector/index.tsx
+++ b/superset-frontend/src/components/DatabaseSelector/index.tsx
@@ -238,23 +238,20 @@ export function DatabaseSelector({
     }
   }
 
+  const shouldFetchSchemas =
+    currentDb?.value &&
+    (!showCatalogSelector || (currentCatalog && currentCatalog.value));
+
   const {
     currentData: schemaData,
     isFetching: loadingSchemas,
     refetch: refetchSchemas,
   } = useSchemas({
-    dbId: currentDb?.value,
+    dbId: shouldFetchSchemas ? currentDb?.value : undefined,
     catalog: currentCatalog?.value,
-    onSuccess: (schemas, isFetched) => {
+    onSuccess: (schemas, isFetched, defaultValue) => {
       setErrorPayload(null);
-      if (schemas.length === 1) {
-        changeSchema(schemas[0]);
-      } else if (
-        !schemas.find(schemaOption => schemaRef.current === schemaOption.value)
-      ) {
-        changeSchema(undefined);
-      }
-
+      autoSelectSchema(schemas, defaultValue);
       if (isFetched) {
         addSuccessToast('List refreshed');
       }
@@ -268,11 +265,13 @@ export function DatabaseSelector({
     },
   });
 
-  const schemaOptions = schemaData || EMPTY_SCHEMA_OPTIONS;
+  const schemaOptions = schemaData?.result || EMPTY_SCHEMA_OPTIONS;
 
   function changeCatalog(catalog: CatalogOption | null | undefined) {
     setCurrentCatalog(catalog);
     setCurrentSchema(undefined);
+    // Clear schema ref so auto-selection works for the new catalog
+    schemaRef.current = undefined;
     if (onCatalogChange && catalog?.value !== catalogRef.current) {
       onCatalogChange(catalog?.value);
     }
@@ -284,10 +283,38 @@ export function DatabaseSelector({
     refetch: refetchCatalogs,
   } = useCatalogs({
     dbId: showCatalogSelector ? currentDb?.value : undefined,
-    onSuccess: (catalogs, isFetched) => {
+    onSuccess: (catalogs, isFetched, defaultValue) => {
       setErrorPayload(null);
+      autoSelectCatalog(catalogs, defaultValue);
+      if (showCatalogSelector && isFetched) {
+        addSuccessToast('List refreshed');
+      }
+    },
+    onError: error => {
+      if (showCatalogSelector) {
+        if (error?.errors) {
+          setErrorPayload(error?.errors?.[0]);
+        } else {
+          handleError(t('There was an error loading the catalogs'));
+        }
+      }
+    },
+  });
+
+  const catalogOptions = catalogData?.result || EMPTY_CATALOG_OPTIONS;
+
+  // Centralized auto-selection logic
+  const autoSelectCatalog = useCallback(
+    (catalogs: CatalogOption[], defaultValue?: string | null) => {
       if (!showCatalogSelector) {
         changeCatalog(null);
+      } else if (defaultValue && !catalogRef.current) {
+        const defaultCatalog = catalogs.find(
+          catalog => catalog.value === defaultValue,
+        );
+        if (defaultCatalog) {
+          changeCatalog(defaultCatalog);
+        }
       } else if (catalogs.length === 1) {
         changeCatalog(catalogs[0]);
       } else if (
@@ -297,23 +324,49 @@ export function DatabaseSelector({
       ) {
         changeCatalog(undefined);
       }
-
-      if (showCatalogSelector && isFetched) {
-        addSuccessToast('List refreshed');
-      }
     },
-    onError: error => {
-      if (showCatalogSelector) {
-        if (error?.errors) {
-          setErrorPayload(error?.errors?.[0]);
-        } else {
-          handleError(t('There was an error loading the catalogs'));
+    [showCatalogSelector, catalogRef],
+  );
+
+  const autoSelectSchema = useCallback(
+    (schemas: SchemaOption[], defaultValue?: string | null) => {
+      if (defaultValue && !schemaRef.current) {
+        const defaultSchema = schemas.find(
+          schema => schema.value === defaultValue,
+        );
+        if (defaultSchema) {
+          changeSchema(defaultSchema);
         }
+      } else if (schemas.length === 1) {
+        changeSchema(schemas[0]);
+      } else if (
+        !schemas.find(schemaOption => schemaRef.current === schemaOption.value)
+      ) {
+        changeSchema(undefined);
       }
     },
-  });
+    [schemaRef],
+  );
 
-  const catalogOptions = catalogData || EMPTY_CATALOG_OPTIONS;
+  // For non-catalog databases, set catalog to null immediately
+  useEffect(() => {
+    if (currentDb && !showCatalogSelector) {
+      setCurrentCatalog(null);
+    }
+  }, [currentDb?.id, showCatalogSelector]);
+
+  // Auto-select when data becomes available (handles both fresh data and 
cached data)
+  useEffect(() => {
+    if (catalogData?.result) {
+      autoSelectCatalog(catalogData.result, catalogData.default);
+    }
+  }, [catalogData?.result, catalogData?.default, autoSelectCatalog]);
+
+  useEffect(() => {
+    if (schemaData?.result) {
+      autoSelectSchema(schemaData.result, schemaData.default);
+    }
+  }, [schemaData?.result, schemaData?.default, autoSelectSchema]);
 
   function changeDatabase(
     value: { label: string; value: number },
@@ -325,6 +378,9 @@ export function DatabaseSelector({
     setCurrentDb(databaseWithId);
     setCurrentCatalog(undefined);
     setCurrentSchema(undefined);
+    // Clear refs so auto-selection works when switching back to a database
+    catalogRef.current = undefined;
+    schemaRef.current = undefined;
     if (onDbChange) {
       onDbChange(databaseWithId);
     }
@@ -402,7 +458,11 @@ export function DatabaseSelector({
     return renderSelectRow(
       <Select
         ariaLabel={t('Select schema or type to search schemas')}
-        disabled={!currentDb || readOnly}
+        disabled={
+          !currentDb ||
+          readOnly ||
+          (showCatalogSelector && (loadingCatalogs || !currentCatalog))
+        }
         header={<FormLabel>{t('Schema')}</FormLabel>}
         labelInValue
         loading={loadingSchemas}
diff --git a/superset-frontend/src/hooks/apiResources/catalogs.ts 
b/superset-frontend/src/hooks/apiResources/catalogs.ts
index 26f56b1dd6..7e759245a6 100644
--- a/superset-frontend/src/hooks/apiResources/catalogs.ts
+++ b/superset-frontend/src/hooks/apiResources/catalogs.ts
@@ -27,10 +27,19 @@ export type CatalogOption = {
   title: string;
 };
 
+export type CatalogResponse = {
+  result: CatalogOption[];
+  default: string | null;
+};
+
 export type FetchCatalogsQueryParams = {
   dbId?: string | number;
   forceRefresh: boolean;
-  onSuccess?: (data: CatalogOption[], isRefetched: boolean) => void;
+  onSuccess?: (
+    data: CatalogOption[],
+    isRefetched: boolean,
+    defaultValue?: string | null,
+  ) => void;
   onError?: (error: ClientErrorObject) => void;
 };
 
@@ -38,19 +47,21 @@ type Params = Omit<FetchCatalogsQueryParams, 
'forceRefresh'>;
 
 const catalogApi = api.injectEndpoints({
   endpoints: builder => ({
-    catalogs: builder.query<CatalogOption[], FetchCatalogsQueryParams>({
+    catalogs: builder.query<CatalogResponse, FetchCatalogsQueryParams>({
       providesTags: [{ type: 'Catalogs', id: 'LIST' }],
       query: ({ dbId, forceRefresh }) => ({
         endpoint: `/api/v1/database/${dbId}/catalogs/`,
         urlParams: {
           force: forceRefresh,
         },
-        transformResponse: ({ json }: JsonResponse) =>
-          json.result.sort().map((value: string) => ({
+        transformResponse: ({ json }: JsonResponse) => ({
+          result: json.result.sort().map((value: string) => ({
             value,
             label: value,
             title: value,
           })),
+          default: json.default,
+        }),
       }),
       serializeQueryArgs: ({ queryArgs: { dbId } }) => ({
         dbId,
@@ -89,7 +100,11 @@ export function useCatalogs(options: Params) {
       if (dbId && (!result.currentData || forceRefresh)) {
         trigger({ dbId, forceRefresh }).then(({ isSuccess, isError, data }) => 
{
           if (isSuccess) {
-            onSuccess?.(data || EMPTY_CATALOGS, forceRefresh);
+            onSuccess?.(
+              data?.result || EMPTY_CATALOGS,
+              forceRefresh,
+              data?.default,
+            );
           }
           if (isError) {
             onError?.(result.error as ClientErrorObject);
diff --git a/superset-frontend/src/hooks/apiResources/schemas.test.ts 
b/superset-frontend/src/hooks/apiResources/schemas.test.ts
index 85751a2fee..74ac610e9a 100644
--- a/superset-frontend/src/hooks/apiResources/schemas.test.ts
+++ b/superset-frontend/src/hooks/apiResources/schemas.test.ts
@@ -28,29 +28,41 @@ import { useSchemas } from './schemas';
 
 const fakeApiResult = {
   result: ['test schema 1', 'test schema b'],
+  default: 'test schema 1',
 };
 const fakeApiResult2 = {
   result: ['test schema 2', 'test schema a'],
+  default: null,
 };
 const fakeApiResult3 = {
   result: ['test schema 3', 'test schema c'],
+  default: 'test schema 3',
 };
 
-const expectedResult = fakeApiResult.result.map((value: string) => ({
-  value,
-  label: value,
-  title: value,
-}));
-const expectedResult2 = fakeApiResult2.result.map((value: string) => ({
-  value,
-  label: value,
-  title: value,
-}));
-const expectedResult3 = fakeApiResult3.result.map((value: string) => ({
-  value,
-  label: value,
-  title: value,
-}));
+const expectedResult = {
+  result: fakeApiResult.result.map((value: string) => ({
+    value,
+    label: value,
+    title: value,
+  })),
+  default: fakeApiResult.default,
+};
+const expectedResult2 = {
+  result: fakeApiResult2.result.map((value: string) => ({
+    value,
+    label: value,
+    title: value,
+  })),
+  default: fakeApiResult2.default,
+};
+const expectedResult3 = {
+  result: fakeApiResult3.result.map((value: string) => ({
+    value,
+    label: value,
+    title: value,
+  })),
+  default: fakeApiResult3.default,
+};
 
 describe('useSchemas hook', () => {
   beforeEach(() => {
diff --git a/superset-frontend/src/hooks/apiResources/schemas.ts 
b/superset-frontend/src/hooks/apiResources/schemas.ts
index 4439b894cf..b56877bebb 100644
--- a/superset-frontend/src/hooks/apiResources/schemas.ts
+++ b/superset-frontend/src/hooks/apiResources/schemas.ts
@@ -27,11 +27,20 @@ export type SchemaOption = {
   title: string;
 };
 
+export type SchemaResponse = {
+  result: SchemaOption[];
+  default: string | null;
+};
+
 export type FetchSchemasQueryParams = {
   dbId?: string | number;
   catalog?: string;
   forceRefresh: boolean;
-  onSuccess?: (data: SchemaOption[], isRefetched: boolean) => void;
+  onSuccess?: (
+    data: SchemaOption[],
+    isRefetched: boolean,
+    defaultValue?: string | null,
+  ) => void;
   onError?: (error: ClientErrorObject) => void;
 };
 
@@ -39,7 +48,7 @@ type Params = Omit<FetchSchemasQueryParams, 'forceRefresh'>;
 
 const schemaApi = api.injectEndpoints({
   endpoints: builder => ({
-    schemas: builder.query<SchemaOption[], FetchSchemasQueryParams>({
+    schemas: builder.query<SchemaResponse, FetchSchemasQueryParams>({
       providesTags: [{ type: 'Schemas', id: 'LIST' }],
       query: ({ dbId, catalog, forceRefresh }) => ({
         endpoint: `/api/v1/database/${dbId}/schemas/`,
@@ -48,12 +57,14 @@ const schemaApi = api.injectEndpoints({
           force: forceRefresh,
           ...(catalog !== undefined && { catalog }),
         },
-        transformResponse: ({ json }: JsonResponse) =>
-          json.result.sort().map((value: string) => ({
+        transformResponse: ({ json }: JsonResponse) => ({
+          result: json.result.sort().map((value: string) => ({
             value,
             label: value,
             title: value,
           })),
+          default: json.default,
+        }),
       }),
       serializeQueryArgs: ({ queryArgs: { dbId, catalog } }) => ({
         dbId,
@@ -98,7 +109,11 @@ export function useSchemas(options: Params) {
         trigger({ dbId, catalog, forceRefresh }).then(
           ({ isSuccess, isError, data }) => {
             if (isSuccess) {
-              onSuccess?.(data || EMPTY_SCHEMAS, forceRefresh);
+              onSuccess?.(
+                data?.result || EMPTY_SCHEMAS,
+                forceRefresh,
+                data?.default,
+              );
             }
             if (isError) {
               onError?.(result.error as ClientErrorObject);
diff --git a/superset-frontend/src/hooks/apiResources/tables.ts 
b/superset-frontend/src/hooks/apiResources/tables.ts
index 81792b4a52..985bd3de73 100644
--- a/superset-frontend/src/hooks/apiResources/tables.ts
+++ b/superset-frontend/src/hooks/apiResources/tables.ts
@@ -172,7 +172,7 @@ export function useTables(options: Params) {
     catalog: catalog || undefined,
   });
   const schemaOptionsMap = useMemo(
-    () => new Set(schemaOptions?.map(({ value }) => value)),
+    () => new Set(schemaOptions?.result?.map(({ value }) => value)),
     [schemaOptions],
   );
 

Reply via email to