codeant-ai-for-open-source[bot] commented on code in PR #36754:
URL: https://github.com/apache/superset/pull/36754#discussion_r2633367970


##########
superset-frontend/src/components/DatabaseSelector/index.tsx:
##########
@@ -271,9 +266,41 @@ export function DatabaseSelector({
 
   const schemaOptions = schemaData || EMPTY_SCHEMA_OPTIONS;
 
+  // Handle schema auto-selection when data changes
+  useEffect(() => {
+    if (!schemaData || loadingSchemas) return;
+
+    setErrorPayload(null);
+
+    if (schemaData.length === 1) {
+      changeSchema(schemaData[0]);
+    } else if (
+      !schemaData.find(schemaOption => schemaRef.current === 
schemaOption.value)

Review Comment:
   **Suggestion:** The schema auto-selection effect checks whether the "current 
selection" is present in the returned schema list using the `schema` prop ref 
(`schemaRef.current`) rather than the actually selected `currentSchema`, so 
when the user changes the schema without the parent feeding that value back via 
the `schema` prop (uncontrolled usage), a refresh or data change can 
incorrectly treat the user's selection as missing and clear or override it 
(e.g., with the default schema). [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
         !schemaData.find(
           schemaOption => currentSchema?.value === schemaOption.value,
         )
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The PR's new effect compares schemaRef.current (which tracks the incoming 
prop `schema`) against the returned schema list. That means an uncontrolled 
usage where the user picks a schema (stored in currentSchema) but the parent 
doesn't update the `schema` prop will appear "missing" on refresh and get 
cleared or replaced by defaults. Replacing the prop-ref check with a check 
against currentSchema (the component's selected state) correctly represents the 
user's actual selection and prevents accidental overrides. This fixes a real 
logic bug, not a cosmetic change.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/components/DatabaseSelector/index.tsx
   **Line:** 278:278
   **Comment:**
        *Logic Error: The schema auto-selection effect checks whether the 
"current selection" is present in the returned schema list using the `schema` 
prop ref (`schemaRef.current`) rather than the actually selected 
`currentSchema`, so when the user changes the schema without the parent feeding 
that value back via the `schema` prop (uncontrolled usage), a refresh or data 
change can incorrectly treat the user's selection as missing and clear or 
override it (e.g., with the default schema).
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/components/DatabaseSelector/index.tsx:
##########
@@ -316,6 +331,49 @@ export function DatabaseSelector({
 
   const catalogOptions = catalogData || EMPTY_CATALOG_OPTIONS;
 
+  // Handle catalog auto-selection when data changes
+  useEffect(() => {
+    if (loadingCatalogs) return;
+
+    setErrorPayload(null);
+
+    if (!showCatalogSelector) {
+      // Only clear catalog if it's not already null
+      if (currentCatalog !== null) {
+        setCurrentCatalog(null);
+        if (onCatalogChange && catalogRef.current != null) {
+          onCatalogChange(undefined);
+        }
+      }
+    } else if (catalogData && catalogData.length === 1) {
+      changeCatalog(catalogData[0]);
+    } else if (
+      catalogData &&
+      !catalogData.find(
+        catalogOption => catalogRef.current === catalogOption.value,

Review Comment:
   **Suggestion:** The catalog auto-selection effect similarly uses the 
`catalog` prop ref (`catalogRef.current`) to decide whether the "current 
selection" exists in the catalog list, rather than the `currentCatalog` state, 
so in uncontrolled usage where the parent does not keep `catalog` in sync, a 
refresh or data change can incorrectly treat the selected catalog as missing 
and clear or replace it (including overriding a user-picked catalog with the 
default or `undefined`). [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
           catalogOption => currentCatalog?.value === catalogOption.value,
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The catalog effect currently uses catalogRef.current (the incoming prop) to 
determine whether the current selection exists in the returned catalog list. As 
with schemas, in uncontrolled usage the user's selection is kept in 
currentCatalog state; using the prop ref causes spurious clears/replacements on 
refresh. Using currentCatalog?.value here is the correct fix — it references 
what the user actually selected and prevents accidental overriding with 
defaults or undefined. This is a functional bugfix.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/components/DatabaseSelector/index.tsx
   **Line:** 353:353
   **Comment:**
        *Logic Error: The catalog auto-selection effect similarly uses the 
`catalog` prop ref (`catalogRef.current`) to decide whether the "current 
selection" exists in the catalog list, rather than the `currentCatalog` state, 
so in uncontrolled usage where the parent does not keep `catalog` in sync, a 
refresh or data change can incorrectly treat the selected catalog as missing 
and clear or replace it (including overriding a user-picked catalog with the 
default or `undefined`).
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/hooks/apiResources/catalogs.test.ts:
##########
@@ -0,0 +1,204 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import rison from 'rison';
+import fetchMock from 'fetch-mock';
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+  createWrapper,
+  defaultStore as store,
+} from 'spec/helpers/testing-library';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { useCatalogs } from './catalogs';
+
+const fakeApiResult = {
+  result: ['catalog_a', 'catalog_b'],
+  default: 'catalog_a',
+};
+const fakeApiResult2 = {
+  result: ['catalog_c', 'catalog_d'],
+  default: null,
+};
+
+const expectedResult = fakeApiResult.result.map((value: string) => ({
+  value,
+  label: value,
+  title: value,
+}));
+const expectedResult2 = fakeApiResult2.result.map((value: string) => ({
+  value,
+  label: value,
+  title: value,
+}));
+
+// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
+describe('useCatalogs hook', () => {
+  beforeEach(() => {
+    fetchMock.reset();
+    store.dispatch(api.util.resetApiState());
+  });
+
+  test('returns api response mapping json result with default catalog', async 
() => {
+    const expectDbId = 'db1';
+    const forceRefresh = false;
+    const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
+    fetchMock.get(catalogApiRoute, fakeApiResult);
+    const onSuccess = jest.fn();
+    const { result, waitFor } = renderHook(
+      () =>
+        useCatalogs({
+          dbId: expectDbId,
+          onSuccess,
+        }),
+      {
+        wrapper: createWrapper({
+          useRedux: true,
+          store,
+        }),
+      },
+    );
+    await waitFor(() =>
+      expect(fetchMock.calls(catalogApiRoute).length).toBe(1),
+    );
+    expect(result.current.data).toEqual(expectedResult);
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+    expect(
+      fetchMock.calls(
+        `end:/api/v1/database/${expectDbId}/catalogs/?q=${rison.encode({
+          force: forceRefresh,
+        })}`,
+      ).length,
+    ).toBe(1);
+    expect(onSuccess).toHaveBeenCalledTimes(1);
+    act(() => {
+      result.current.refetch();
+    });
+    await waitFor(() =>
+      expect(fetchMock.calls(catalogApiRoute).length).toBe(2),
+    );
+    expect(
+      fetchMock.calls(
+        `end:/api/v1/database/${expectDbId}/catalogs/?q=${rison.encode({
+          force: true,
+        })}`,
+      ).length,
+    ).toBe(1);
+    expect(onSuccess).toHaveBeenCalledTimes(2);
+    expect(result.current.data).toEqual(expectedResult);
+    expect(result.current.defaultCatalog).toBe('catalog_a');

Review Comment:
   **Suggestion:** After calling `refetch`, the test again only waits for the 
fetch call count to reach 2 before checking the URL, `onSuccess` call count, 
and hook data/default, so these assertions can run before the refetch state 
update has completed; moving all related expectations into a single `waitFor` 
removes this async race and stabilizes the test. [race condition]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       await waitFor(() => {
         expect(fetchMock.calls(catalogApiRoute).length).toBe(2);
         expect(
           fetchMock.calls(
             `end:/api/v1/database/${expectDbId}/catalogs/?q=${rison.encode({
               force: true,
             })}`,
           ).length,
         ).toBe(1);
         expect(onSuccess).toHaveBeenCalledTimes(2);
         expect(result.current.data).toEqual(expectedResult);
         expect(result.current.defaultCatalog).toBe('catalog_a');
       });
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   After triggering refetch, the test again waits only on the network call 
count. The hook's internal state (data, defaultCatalog) and the onSuccess 
handler invocation may not be fully observed at that exact moment. Grouping 
these expectations inside a single waitFor ensures the assertions run only 
after the hook finishes updating, reducing intermittent failures. This is a 
valid stability improvement.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/hooks/apiResources/catalogs.test.ts
   **Line:** 91:103
   **Comment:**
        *Race Condition: After calling `refetch`, the test again only waits for 
the fetch call count to reach 2 before checking the URL, `onSuccess` call 
count, and hook data/default, so these assertions can run before the refetch 
state update has completed; moving all related expectations into a single 
`waitFor` removes this async race and stabilizes the test.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/hooks/apiResources/tables.ts:
##########
@@ -167,7 +167,7 @@ export const {
 export function useTables(options: Params) {
   const { dbId, catalog, schema, onSuccess, onError } = options || {};
   const isMountedRef = useRef(false);
-  const { currentData: schemaOptions, isFetching } = useSchemas({
+  const { data: schemaOptions, isFetching } = useSchemas({

Review Comment:
   **Suggestion:** When `useSchemas` has not yet loaded data, `schemaOptions` 
is `undefined`, so `schemaOptions?.map(...)` evaluates to `undefined` and `new 
Set(undefined)` will throw a runtime `TypeError` because `undefined` is not 
iterable; defaulting `schemaOptions` to an empty array ensures the `Set` 
constructor always receives a valid iterable. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
     const { data: schemaOptions = [], isFetching } = useSchemas({
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   schemaOptions is used immediately after in:
     const schemaOptionsMap = useMemo(
       () => new Set(schemaOptions?.map(({ value }) => value)),
       [schemaOptions],
     );
   If schemaOptions is undefined while useSchemas is still loading, 
schemaOptions?.map(...) returns undefined and new Set(undefined) will throw 
"TypeError: undefined is not iterable". Defaulting schemaOptions to an empty 
array (data: schemaOptions = []) guarantees the Set constructor always receives 
a valid iterable and avoids a real runtime crash. This is a real bug fix, not 
just cosmetic.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/hooks/apiResources/tables.ts
   **Line:** 170:170
   **Comment:**
        *Logic Error: When `useSchemas` has not yet loaded data, 
`schemaOptions` is `undefined`, so `schemaOptions?.map(...)` evaluates to 
`undefined` and `new Set(undefined)` will throw a runtime `TypeError` because 
`undefined` is not iterable; defaulting `schemaOptions` to an empty array 
ensures the `Set` constructor always receives a valid iterable.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/hooks/apiResources/catalogs.test.ts:
##########
@@ -0,0 +1,204 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import rison from 'rison';
+import fetchMock from 'fetch-mock';
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+  createWrapper,
+  defaultStore as store,
+} from 'spec/helpers/testing-library';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { useCatalogs } from './catalogs';
+
+const fakeApiResult = {
+  result: ['catalog_a', 'catalog_b'],
+  default: 'catalog_a',
+};
+const fakeApiResult2 = {
+  result: ['catalog_c', 'catalog_d'],
+  default: null,
+};
+
+const expectedResult = fakeApiResult.result.map((value: string) => ({
+  value,
+  label: value,
+  title: value,
+}));
+const expectedResult2 = fakeApiResult2.result.map((value: string) => ({
+  value,
+  label: value,
+  title: value,
+}));
+
+// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
+describe('useCatalogs hook', () => {
+  beforeEach(() => {
+    fetchMock.reset();
+    store.dispatch(api.util.resetApiState());
+  });
+
+  test('returns api response mapping json result with default catalog', async 
() => {
+    const expectDbId = 'db1';
+    const forceRefresh = false;
+    const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
+    fetchMock.get(catalogApiRoute, fakeApiResult);
+    const onSuccess = jest.fn();
+    const { result, waitFor } = renderHook(
+      () =>
+        useCatalogs({
+          dbId: expectDbId,
+          onSuccess,
+        }),
+      {
+        wrapper: createWrapper({
+          useRedux: true,
+          store,
+        }),
+      },
+    );
+    await waitFor(() =>
+      expect(fetchMock.calls(catalogApiRoute).length).toBe(1),
+    );
+    expect(result.current.data).toEqual(expectedResult);
+    expect(result.current.defaultCatalog).toBe('catalog_a');

Review Comment:
   **Suggestion:** The first test waits only for the fetch call count before 
asserting on `result.current.data` and `result.current.defaultCatalog`, so 
there's a race condition where the network request is recorded but the hook 
state hasn't updated yet, leading to flaky or intermittently failing 
assertions; wrapping all these expectations inside the same `waitFor` ensures 
they only run after React state has settled. [race condition]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       await waitFor(() => {
         expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
         expect(result.current.data).toEqual(expectedResult);
         expect(result.current.defaultCatalog).toBe('catalog_a');
       });
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The suggestion correctly points out a real race: the test waits only for the 
mock network call to be recorded but then immediately asserts on hook state, 
which may not have updated yet. Combining the fetch-count check and the state 
assertions inside the same waitFor ensures React state updates have settled 
before making assertions and reduces flakiness. This is a behavioral test 
stabilization, not a stylistic change.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/hooks/apiResources/catalogs.test.ts
   **Line:** 75:79
   **Comment:**
        *Race Condition: The first test waits only for the fetch call count 
before asserting on `result.current.data` and `result.current.defaultCatalog`, 
so there's a race condition where the network request is recorded but the hook 
state hasn't updated yet, leading to flaky or intermittently failing 
assertions; wrapping all these expectations inside the same `waitFor` ensures 
they only run after React state has settled.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset/databases/api.py:
##########
@@ -795,23 +832,30 @@ def schemas(self, pk: int, **kwargs: Any) -> 
FlaskResponse:
                 catalog,
                 schemas,
             )
+            default_schema = self._get_default_schema(database, catalog, 
schemas, pk)
+
             if params.get("upload_allowed"):
                 if not database.allow_file_upload:
-                    return self.response(200, result=[])
+                    return self.response(200, result=[], default=None)
                 if allowed_schemas := 
database.get_schema_access_for_file_upload():
                     # some databases might return the list of schemas in 
uppercase,
                     # while the list of allowed schemas is manually inputted so
                     # could be lowercase
                     allowed_schemas = {schema.lower() for schema in 
allowed_schemas}
+                    filtered_schemas = [
+                        schema
+                        for schema in schemas
+                        if schema.lower() in allowed_schemas
+                    ]
+                    # Check if default is in filtered list
+                    if default_schema and default_schema.lower() not in 
allowed_schemas:
+                        default_schema = None
                     return self.response(
                         200,
-                        result=[
-                            schema
-                            for schema in schemas
-                            if schema.lower() in allowed_schemas
-                        ],
+                        result=filtered_schemas,
+                        default=default_schema,
                     )

Review Comment:
   **Suggestion:** When the `upload_allowed` filter is requested but the 
database is configured for file upload with no schemas explicitly allowed 
(empty or falsy `get_schema_access_for_file_upload()`), the endpoint silently 
ignores `upload_allowed` and returns all accessible schemas with a default, 
which misrepresents which schemas actually accept uploads and can confuse the 
UI. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
                       # No schemas are configured for upload; honor the 
upload_allowed filter
                       return self.response(200, result=[], default=None)
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The current flow silently falls through to returning all accessible schemas 
when upload_allowed is requested but get_schema_access_for_file_upload() 
returns empty/false. That misrepresents which schemas accept uploads. Returning 
an empty list (and None default) when no upload-allowed schemas are configured 
is the correct, least-surprising behavior for an "upload_allowed" filter and 
fixes a real logic/UX bug.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/databases/api.py
   **Line:** 855:857
   **Comment:**
        *Logic Error: When the `upload_allowed` filter is requested but the 
database is configured for file upload with no schemas explicitly allowed 
(empty or falsy `get_schema_access_for_file_upload()`), the endpoint silently 
ignores `upload_allowed` and returns all accessible schemas with a default, 
which misrepresents which schemas actually accept uploads and can confuse the 
UI.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset/databases/api.py:
##########
@@ -317,6 +317,32 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         "changed_by": [["id", BaseFilterRelatedUsers, lambda: []]],
     }
 
+    @staticmethod
+    def _get_default_schema(
+        database: Database,
+        catalog: str | None,
+        accessible_schemas: set[str],
+        pk: int,
+    ) -> str | None:
+        """
+        Get the default schema for a database/catalog, with error handling.
+
+        Returns None if the default cannot be determined or is not accessible.
+        """
+        try:
+            default_schema = database.get_default_schema(catalog)

Review Comment:
   **Suggestion:** The helper for computing the default schema ignores the 
database's configured default catalog when the `catalog` argument is `None`, 
which can cause the returned default schema to be inconsistent with the schema 
set used by the security manager and therefore point to the wrong schema on 
engines where the default schema depends on the default catalog. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
               # Normalize to the database default catalog when none is 
provided,
               # to keep behavior consistent with permission checks.
               effective_catalog = catalog or database.get_default_catalog()
               default_schema = database.get_default_schema(effective_catalog)
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   Normalizing an absent catalog to the database's default catalog before 
asking for the default schema is a defensible behavioral change and prevents 
inconsistencies between permission checks and the computed default schema. The 
existing implementation can return a schema computed against "no catalog" while 
downstream checks use schemas filtered by catalog-aware logic, which can cause 
the default to be omitted or be incorrect. The proposed change is a real logic 
improvement (not just stylistic) that aligns behavior and reduces surprising UI 
behavior.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/databases/api.py
   **Line:** 333:333
   **Comment:**
        *Logic Error: The helper for computing the default schema ignores the 
database's configured default catalog when the `catalog` argument is `None`, 
which can cause the returned default schema to be inconsistent with the schema 
set used by the security manager and therefore point to the wrong schema on 
engines where the default schema depends on the default catalog.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/hooks/apiResources/catalogs.test.ts:
##########
@@ -0,0 +1,204 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import rison from 'rison';
+import fetchMock from 'fetch-mock';
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+  createWrapper,
+  defaultStore as store,
+} from 'spec/helpers/testing-library';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { useCatalogs } from './catalogs';
+
+const fakeApiResult = {
+  result: ['catalog_a', 'catalog_b'],
+  default: 'catalog_a',
+};
+const fakeApiResult2 = {
+  result: ['catalog_c', 'catalog_d'],
+  default: null,
+};
+
+const expectedResult = fakeApiResult.result.map((value: string) => ({
+  value,
+  label: value,
+  title: value,
+}));
+const expectedResult2 = fakeApiResult2.result.map((value: string) => ({
+  value,
+  label: value,
+  title: value,
+}));
+
+// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
+describe('useCatalogs hook', () => {
+  beforeEach(() => {
+    fetchMock.reset();
+    store.dispatch(api.util.resetApiState());
+  });
+
+  test('returns api response mapping json result with default catalog', async 
() => {
+    const expectDbId = 'db1';
+    const forceRefresh = false;
+    const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
+    fetchMock.get(catalogApiRoute, fakeApiResult);
+    const onSuccess = jest.fn();
+    const { result, waitFor } = renderHook(
+      () =>
+        useCatalogs({
+          dbId: expectDbId,
+          onSuccess,
+        }),
+      {
+        wrapper: createWrapper({
+          useRedux: true,
+          store,
+        }),
+      },
+    );
+    await waitFor(() =>
+      expect(fetchMock.calls(catalogApiRoute).length).toBe(1),
+    );
+    expect(result.current.data).toEqual(expectedResult);
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+    expect(
+      fetchMock.calls(
+        `end:/api/v1/database/${expectDbId}/catalogs/?q=${rison.encode({
+          force: forceRefresh,
+        })}`,
+      ).length,
+    ).toBe(1);
+    expect(onSuccess).toHaveBeenCalledTimes(1);
+    act(() => {
+      result.current.refetch();
+    });
+    await waitFor(() =>
+      expect(fetchMock.calls(catalogApiRoute).length).toBe(2),
+    );
+    expect(
+      fetchMock.calls(
+        `end:/api/v1/database/${expectDbId}/catalogs/?q=${rison.encode({
+          force: true,
+        })}`,
+      ).length,
+    ).toBe(1);
+    expect(onSuccess).toHaveBeenCalledTimes(2);
+    expect(result.current.data).toEqual(expectedResult);
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+  });
+
+  test('returns cached data without api request', async () => {
+    const expectDbId = 'db1';
+    const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
+    fetchMock.get(catalogApiRoute, fakeApiResult);
+    const { result, rerender, waitFor } = renderHook(
+      () =>
+        useCatalogs({
+          dbId: expectDbId,
+        }),
+      {
+        wrapper: createWrapper({
+          useRedux: true,
+          store,
+        }),
+      },
+    );
+    await waitFor(() => expect(result.current.data).toEqual(expectedResult));
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+    expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
+    rerender();
+    await waitFor(() => expect(result.current.data).toEqual(expectedResult));
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+    expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
+  });
+
+  test('returns refreshed data after switching databases', async () => {
+    const expectDbId = 'db1';
+    const catalogApiRoute = `glob:*/api/v1/database/*/catalogs/*`;
+    fetchMock.get(catalogApiRoute, url =>
+      url.includes(expectDbId) ? fakeApiResult : fakeApiResult2,
+    );
+    const onSuccess = jest.fn();
+    const { result, rerender, waitFor } = renderHook(
+      ({ dbId }) =>
+        useCatalogs({
+          dbId,
+          onSuccess,
+        }),
+      {
+        initialProps: { dbId: expectDbId },
+        wrapper: createWrapper({
+          useRedux: true,
+          store,
+        }),
+      },
+    );
+
+    await waitFor(() => expect(result.current.data).toEqual(expectedResult));
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+    expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
+    expect(onSuccess).toHaveBeenCalledTimes(1);
+
+    rerender({ dbId: 'db2' });
+    await waitFor(() => expect(result.current.data).toEqual(expectedResult2));
+    expect(result.current.defaultCatalog).toBeNull();
+    expect(fetchMock.calls(catalogApiRoute).length).toBe(2);
+    expect(onSuccess).toHaveBeenCalledTimes(2);
+
+    rerender({ dbId: expectDbId });
+    await waitFor(() => expect(result.current.data).toEqual(expectedResult));
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+    expect(fetchMock.calls(catalogApiRoute).length).toBe(2);
+    expect(onSuccess).toHaveBeenCalledTimes(2);
+
+    // clean up cache
+    act(() => {
+      store.dispatch(api.util.invalidateTags(['Catalogs']));
+    });
+
+    await waitFor(() =>
+      expect(fetchMock.calls(catalogApiRoute).length).toBe(4),
+    );
+    expect(fetchMock.calls(catalogApiRoute)[2][0]).toContain(expectDbId);
+    await waitFor(() => expect(result.current.data).toEqual(expectedResult));
+    expect(result.current.defaultCatalog).toBe('catalog_a');
+  });
+
+  test('returns null defaultCatalog when API response has no default', async 
() => {
+    const expectDbId = 'db-no-default';
+    const catalogApiRoute = `glob:*/api/v1/database/${expectDbId}/catalogs/*`;
+    fetchMock.get(catalogApiRoute, { result: ['catalog1', 'catalog2'] });
+    const { result, waitFor } = renderHook(
+      () =>
+        useCatalogs({
+          dbId: expectDbId,
+        }),
+      {
+        wrapper: createWrapper({
+          useRedux: true,
+          store,
+        }),
+      },
+    );
+    await waitFor(() =>
+      expect(fetchMock.calls(catalogApiRoute).length).toBe(1),
+    );
+    expect(result.current.defaultCatalog).toBeNull();

Review Comment:
   **Suggestion:** The "no default" test currently only waits for the fetch 
call count before asserting `defaultCatalog` is null, so it can pass even if 
the hook never updates its data (because the `?? null` fallback yields null), 
masking real bugs; combining the fetch count and `defaultCatalog` assertion 
inside `waitFor` guarantees the state has been updated and that the hook truly 
returns a null default from the API response. [race condition]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       await waitFor(() => {
         expect(fetchMock.calls(catalogApiRoute).length).toBe(1);
         expect(result.current.defaultCatalog).toBeNull();
       });
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The test validates that defaultCatalog is null but only waits for the fetch 
call to be recorded. If the hook's state hasn't updated (or a fallback produces 
null), the assertion might be a false positive. Moving the defaultCatalog 
assertion into the same waitFor ensures the hook actually produced the null 
default as a result of the API response, making the test meaningful and less 
flaky.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/hooks/apiResources/catalogs.test.ts
   **Line:** 199:202
   **Comment:**
        *Race Condition: The "no default" test currently only waits for the 
fetch call count before asserting `defaultCatalog` is null, so it can pass even 
if the hook never updates its data (because the `?? null` fallback yields 
null), masking real bugs; combining the fetch count and `defaultCatalog` 
assertion inside `waitFor` guarantees the state has been updated and that the 
hook truly returns a null default from the API response.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



##########
superset-frontend/src/hooks/apiResources/schemas.test.ts:
##########
@@ -208,14 +209,37 @@ describe('useSchemas hook', () => {
 
     await waitFor(() => 
expect(fetchMock.calls(schemaApiRoute).length).toBe(1));
     expect(result.current.data).toEqual(expectedResult3);
+    expect(result.current.defaultSchema).toBe('test schema c');
     expect(onSuccess).toHaveBeenCalledTimes(1);
 
     rerender({ dbId, catalog: 'catalog2' });
     await waitFor(() => 
expect(fetchMock.calls(schemaApiRoute).length).toBe(2));
     expect(result.current.data).toEqual(expectedResult2);
+    expect(result.current.defaultSchema).toBeNull();
 
     rerender({ dbId, catalog: expectCatalog });
     expect(result.current.data).toEqual(expectedResult3);
+    expect(result.current.defaultSchema).toBe('test schema c');
     expect(fetchMock.calls(schemaApiRoute).length).toBe(2);
   });
+
+  test('returns null defaultSchema when API response has no default', async () 
=> {
+    const expectDbId = 'db-no-default';
+    const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`;
+    fetchMock.get(schemaApiRoute, { result: ['schema1', 'schema2'] });
+    const { result, waitFor } = renderHook(
+      () =>
+        useSchemas({
+          dbId: expectDbId,
+        }),
+      {
+        wrapper: createWrapper({
+          useRedux: true,
+          store,
+        }),
+      },
+    );
+    await waitFor(() => 
expect(fetchMock.calls(schemaApiRoute).length).toBe(1));
+    expect(result.current.defaultSchema).toBeNull();

Review Comment:
   **Suggestion:** In the "returns null defaultSchema when API response has no 
default" test, you only wait for the fetchMock call count to reach 1 before 
asserting on `defaultSchema`, which can run before the RTK Query state has 
finished updating, making the test flaky; combining the call-count and 
`defaultSchema` checks inside the same `waitFor` ensures the assertion only 
runs once the hook state is settled. [logic error]
   
   **Severity Level:** Minor ⚠️
   ```suggestion
       await waitFor(() => {
         expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
         expect(result.current.defaultSchema).toBeNull();
       });
   ```
   <details>
   <summary><b>Why it matters? ⭐ </b></summary>
   
   The current test only waits for the network call count, then immediately 
asserts on result.current.defaultSchema.
   RTK Query / the hook may still be updating internal state after the fetch 
completes, so separating the two assertions can cause a race and intermittent 
failures.
   Combining both expectations inside the same waitFor ensures the test waits 
until the hook's state reflects the response before asserting, which is a 
legitimate, non-cosmetic stability fix for a flaky test.
   </details>
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset-frontend/src/hooks/apiResources/schemas.test.ts
   **Line:** 242:243
   **Comment:**
        *Logic Error: In the "returns null defaultSchema when API response has 
no default" test, you only wait for the fetchMock call count to reach 1 before 
asserting on `defaultSchema`, which can run before the RTK Query state has 
finished updating, making the test flaky; combining the call-count and 
`defaultSchema` checks inside the same `waitFor` ensures the assertion only 
runs once the hook state is settled.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   ```
   </details>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to