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

betodealmeida pushed a commit to branch semantic-view-dashboard-filters
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 6a82eaded7dc366a6efaffb53eae93e1cec98194
Author: Beto Dealmeida <[email protected]>
AuthorDate: Wed May 27 14:15:03 2026 -0400

    feat(semantic layers): dashboard filters
---
 .../src/superset_core/semantic_layers/types.py     |   2 +-
 superset-frontend/package-lock.json                |   2 +-
 .../superset-ui-core/src/query/types/Dashboard.ts  |   1 +
 .../FilterBar/FilterControls/FilterValue.tsx       |   8 +-
 .../FiltersConfigForm/ColumnSelect.tsx             |  90 +++++++---
 .../FiltersConfigForm/DatasetSelect.test.tsx       |  45 +++++
 .../FiltersConfigForm/DatasetSelect.tsx            |  99 ++++++++++-
 .../FiltersConfigForm/FiltersConfigForm.tsx        | 192 +++++++++++++++------
 .../FiltersConfigForm/getControlItemsMap.tsx       |   3 +
 .../FiltersConfigForm/utils.test.ts                |  74 ++++++++
 .../FiltersConfigModal/FiltersConfigForm/utils.ts  |  90 ++++++++++
 .../transformers/customizationTransformer.ts       |   4 +
 .../transformers/filterTransformer.ts              |   4 +
 .../nativeFilters/FiltersConfigModal/types.ts      |   2 +
 .../nativeFilters/FiltersConfigModal/utils.ts      |   3 +
 .../dashboard/components/nativeFilters/utils.ts    |   5 +-
 .../src/dashboard/reducers/dashboardInfo.test.ts   |  24 ++-
 .../src/dashboard/reducers/dashboardInfo.ts        |   1 +
 .../features/datasets/DatasetSelectLabel/index.tsx |   1 +
 superset/semantic_layers/api.py                    |   9 +-
 superset/semantic_layers/mapper.py                 |   6 +-
 tests/unit_tests/semantic_layers/mapper_test.py    |  27 ++-
 22 files changed, 598 insertions(+), 94 deletions(-)

diff --git a/superset-core/src/superset_core/semantic_layers/types.py 
b/superset-core/src/superset_core/semantic_layers/types.py
index 3bfa9e8c315..c26667b8bb2 100644
--- a/superset-core/src/superset_core/semantic_layers/types.py
+++ b/superset-core/src/superset_core/semantic_layers/types.py
@@ -158,7 +158,7 @@ class Filter:
     type: PredicateType
     column: Dimension | Metric | None
     operator: Operator
-    value: FilterValues | frozenset[FilterValues]
+    value: FilterValues | tuple[FilterValues, ...] | frozenset[FilterValues]
 
 
 class OrderDirection(enum.Enum):
diff --git a/superset-frontend/package-lock.json 
b/superset-frontend/package-lock.json
index 0902547f989..25736c00216 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -50085,7 +50085,7 @@
         "@math.gl/web-mercator": "^4.1.0",
         "mapbox-gl": "^3.24.0",
         "maplibre-gl": "^5.24.0",
-        "react-map-gl": "^8.1.1",
+        "react-map-gl": "^8.1.0",
         "supercluster": "^8.0.1"
       },
       "peerDependencies": {
diff --git 
a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts 
b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
index 3fdf674f4f5..3ad01c1b701 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
@@ -38,6 +38,7 @@ export interface NativeFilterScope {
 export interface NativeFilterTarget {
   datasetId: number;
   column: NativeFilterColumn;
+  datasourceType?: string;
 
   // maybe someday support this?
   // show values from these columns in the filter options selector
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
index c23f2578718..8757f5c7e1b 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -177,8 +177,13 @@ const FilterValue: FC<FilterValueProps> = ({
   const [target] = targets || [];
   const {
     datasetId,
+    datasourceType,
     column = {},
-  }: Partial<{ datasetId: number; column: { name?: string } }> = target || {};
+  }: Partial<{
+    datasetId: number;
+    datasourceType: string;
+    column: { name?: string };
+  }> = target || {};
   const groupby = column?.name;
   const hasDataSource = !!datasetId;
   const [isLoading, setIsLoading] = useState<boolean>(hasDataSource);
@@ -212,6 +217,7 @@ const FilterValue: FC<FilterValueProps> = ({
     const newFormData = getFormData({
       ...filter,
       datasetId,
+      datasourceType,
       dependencies,
       groupby,
       adhoc_filters: adhocFilters,
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
index 2e8f43bb21f..f956e164b76 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
@@ -19,9 +19,11 @@
 import { useCallback, useState, useMemo, useEffect } from 'react';
 import rison from 'rison';
 import { t } from '@apache-superset/core/translation';
+import { GenericDataType } from '@apache-superset/core/common';
 import {
   Column,
   ensureIsArray,
+  JsonResponse,
   useChangeEffect,
   getClientErrorObject,
 } from '@superset-ui/core';
@@ -29,6 +31,7 @@ import { type FormInstance, Select } from 
'@superset-ui/core/components';
 import { useToasts } from 'src/components/MessageToasts/withToasts';
 import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
 import { NativeFiltersForm, NativeFiltersFormItem } from '../types';
+import { mapSemanticTypeToGenericDataType } from './utils';
 
 interface ColumnSelectProps {
   allowClear?: boolean;
@@ -37,6 +40,7 @@ interface ColumnSelectProps {
   formField?: keyof NativeFiltersFormItem;
   filterId: string;
   datasetId?: number;
+  datasourceType?: string;
   value?: string | string[];
   onChange?: (value: string) => void;
   mode?: 'multiple';
@@ -51,6 +55,7 @@ export function ColumnSelect({
   formField = 'column',
   filterId,
   datasetId,
+  datasourceType,
   value,
   onChange,
   mode,
@@ -86,25 +91,68 @@ export function ColumnSelect({
     }
   }, [currentColumn, currentFilterType, resetColumnField]);
 
-  useChangeEffect(datasetId, previous => {
+  // Use a compound key so the effect re-fires when either the dataset ID or
+  // the datasource type changes.  Datasets and semantic views have independent
+  // ID sequences, so switching between them with the same numeric ID must 
still
+  // trigger a column re-fetch.
+  const datasourceKey = `${datasetId}__${datasourceType || 'table'}`;
+  useChangeEffect(datasourceKey, previous => {
     if (previous != null) {
       setColumns([]);
       resetColumnField();
     }
     if (datasetId != null) {
       setLoading(true);
-      cachedSupersetGet({
-        endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({
-          columns: [
-            'columns.column_name',
-            'columns.is_dttm',
-            'columns.type_generic',
-            'columns.filterable',
-          ],
-        })}`,
-      })
-        .then(
-          ({ json: { result } }) => {
+      const handleError = async (
+        badResponse: Parameters<typeof getClientErrorObject>[0],
+      ) => {
+        const { error, message } = await getClientErrorObject(badResponse);
+        let errorText = message || error || t('An error has occurred');
+        if (message === 'Forbidden') {
+          errorText = t('You do not have permission to edit this dashboard');
+        }
+        addDangerToast(errorText);
+      };
+
+      if (datasourceType === 'semantic_view') {
+        cachedSupersetGet({
+          endpoint: `/api/v1/semantic_view/${datasetId}/structure`,
+        })
+          .then((response: JsonResponse) => {
+            const { dimensions = [] } = response.json?.result ?? {};
+            const cols: Column[] = dimensions.map(
+              (dim: { name: string; type: string }) => {
+                const mappedType = mapSemanticTypeToGenericDataType(dim.type);
+                return {
+                  column_name: dim.name,
+                  is_dttm: mappedType === GenericDataType.Temporal,
+                  type_generic: mappedType,
+                  filterable: true,
+                };
+              },
+            );
+            const lookupValue = Array.isArray(value) ? value : [value];
+            const valueExists = cols.some((column: Column) =>
+              lookupValue?.includes(column.column_name),
+            );
+            if (!valueExists) {
+              resetColumnField();
+            }
+            setColumns(cols);
+          }, handleError)
+          .finally(() => setLoading(false));
+      } else {
+        cachedSupersetGet({
+          endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({
+            columns: [
+              'columns.column_name',
+              'columns.is_dttm',
+              'columns.type_generic',
+              'columns.filterable',
+            ],
+          })}`,
+        })
+          .then(({ json: { result } }) => {
             const lookupValue = Array.isArray(value) ? value : [value];
             const valueExists = result.columns.some((column: Column) =>
               lookupValue?.includes(column.column_name),
@@ -113,19 +161,9 @@ export function ColumnSelect({
               resetColumnField();
             }
             setColumns(result.columns);
-          },
-          async badResponse => {
-            const { error, message } = await getClientErrorObject(badResponse);
-            let errorText = message || error || t('An error has occurred');
-            if (message === 'Forbidden') {
-              errorText = t(
-                'You do not have permission to edit this dashboard',
-              );
-            }
-            addDangerToast(errorText);
-          },
-        )
-        .finally(() => setLoading(false));
+          }, handleError)
+          .finally(() => setLoading(false));
+      }
     }
   });
 
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx
index fb8632b09d5..be579c9848b 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.test.tsx
@@ -265,3 +265,48 @@ test('returns total count from API when data is filtered', 
async () => {
   expect(result.data).toHaveLength(2);
   expect(result.data.find(item => item.value === 2)).toBeUndefined();
 });
+
+test('does not exclude semantic views that share dataset IDs', async () => {
+  supersetGetCache.clear();
+  fetchMock.clearHistory().removeRoutes();
+
+  const originalFeatureFlags = window.featureFlags;
+  window.featureFlags = {
+    ...originalFeatureFlags,
+    SEMANTIC_LAYERS: true,
+  };
+
+  try {
+    fetchMock.get('glob:*/api/v1/datasource/*', {
+      result: [
+        {
+          id: 7,
+          table_name: 'orders_dataset',
+          kind: 'physical',
+          database: { database_name: 'examples' },
+          schema: 'public',
+        },
+        {
+          id: 7,
+          table_name: 'orders_semantic_view',
+          kind: 'semantic_view',
+          database: { database_name: 'semantic_layer' },
+          schema: null,
+        },
+      ],
+      count: 2,
+    });
+
+    const result = await loadDatasetOptions('', 0, 100, [7]);
+
+    expect(result.totalCount).toBe(2);
+    expect(result.data).toHaveLength(1);
+    expect(result.data[0]).toMatchObject({
+      value: 'sv:7',
+      kind: 'semantic_view',
+      table_name: 'orders_semantic_view',
+    });
+  } finally {
+    window.featureFlags = originalFeatureFlags;
+  }
+});
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
index d089e98a675..7d1d13bdedf 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
@@ -20,6 +20,8 @@ import { useCallback, useMemo, ReactNode } from 'react';
 import rison from 'rison';
 import { t } from '@apache-superset/core/translation';
 import {
+  isFeatureEnabled,
+  FeatureFlag,
   JsonResponse,
   ClientErrorObject,
   getClientErrorObject,
@@ -37,8 +39,12 @@ import {
 } from 'src/features/semanticLayers/label';
 
 interface DatasetSelectProps {
-  onChange: (value: { label: string | ReactNode; value: number }) => void;
-  value?: { label: string | ReactNode; value: number };
+  onChange: (value: {
+    label: string | ReactNode;
+    value: number;
+    kind?: string;
+  }) => void;
+  value?: { label: string | ReactNode; value: number; kind?: string };
   excludeDatasetIds?: number[];
 }
 
@@ -50,37 +56,78 @@ const getErrorMessage = ({ error, message }: 
ClientErrorObject) => {
   return errorText;
 };
 
+/**
+ * Builds a unique select-option value for the combined datasource endpoint.
+ * Datasets and semantic views have independent integer ID sequences, so we
+ * prefix with a type tag to avoid collisions in AsyncSelect's dedup logic.
+ */
+const toCompositeValue = (id: number, kind?: string): string =>
+  kind === 'semantic_view' ? `sv:${id}` : `ds:${id}`;
+
+/** Extracts the numeric ID from a composite "sv:123" / "ds:456" string. */
+const fromCompositeValue = (compositeValue: string | number): number =>
+  typeof compositeValue === 'string'
+    ? parseInt(compositeValue.split(':')[1], 10)
+    : compositeValue;
+
+/** Derives the `kind` value from a composite string prefix. */
+const kindFromComposite = (compositeValue: string): string | undefined =>
+  compositeValue.startsWith('sv:') ? 'semantic_view' : undefined;
+
+const isExcludedDatasource = (
+  item: Dataset,
+  excludeDatasetIds: number[],
+): boolean => {
+  if (!excludeDatasetIds.includes(item.id)) {
+    return false;
+  }
+
+  return item.kind !== 'semantic_view';
+};
+
 export const loadDatasetOptions = async (
   search: string,
   page: number,
   pageSize: number,
   excludeDatasetIds: number[] = [],
 ) => {
+  const useSemanticLayers = isFeatureEnabled(FeatureFlag.SemanticLayers);
   const query = rison.encode({
-    columns: ['id', 'table_name', 'database.database_name', 'schema'],
+    ...(useSemanticLayers
+      ? {}
+      : {
+          columns: ['id', 'table_name', 'database.database_name', 'schema'],
+        }),
     filters: [{ col: 'table_name', opr: 'ct', value: search }],
     page,
     page_size: pageSize,
     order_column: 'table_name',
     order_direction: 'asc',
   });
+  const endpoint = useSemanticLayers
+    ? `/api/v1/datasource/?q=${query}`
+    : `/api/v1/dataset/?q=${query}`;
   return cachedSupersetGet({
-    endpoint: `/api/v1/dataset/?q=${query}`,
+    endpoint,
   })
     .then((response: JsonResponse) => {
       const filteredResult = response.json.result.filter(
-        (item: Dataset) => !excludeDatasetIds.includes(item.id),
+        (item: Dataset) => !isExcludedDatasource(item, excludeDatasetIds),
       );
 
       const list: {
         label: string | ReactNode;
         value: string | number;
         table_name: string;
+        kind?: string;
       }[] = filteredResult.map((item: Dataset) => ({
         ...item,
         label: DatasetSelectLabel(item),
-        value: item.id,
+        value: useSemanticLayers
+          ? toCompositeValue(item.id, item.kind)
+          : item.id,
         table_name: item.table_name,
+        kind: item.kind,
       }));
       return {
         data: list,
@@ -98,18 +145,54 @@ const DatasetSelect = ({
   value,
   excludeDatasetIds = [],
 }: DatasetSelectProps) => {
+  const useSemanticLayers = isFeatureEnabled(FeatureFlag.SemanticLayers);
+
   const loadDatasetOptionsCallback = useCallback(
     (search: string, page: number, pageSize: number) =>
       loadDatasetOptions(search, page, pageSize, excludeDatasetIds),
     [excludeDatasetIds],
   );
 
+  // Convert the external numeric value to the composite string format that
+  // AsyncSelect needs for matching against the loaded options.
+  const selectValue = useMemo(() => {
+    if (!value || !useSemanticLayers) return value;
+    return {
+      ...value,
+      value: toCompositeValue(value.value, value.kind),
+    };
+  }, [value, useSemanticLayers]);
+
+  // Convert the composite string value from the selected option back to a
+  // numeric ID before passing it to the external onChange handler.
+  // AsyncSelect's first argument is a LabeledValue ({key, label, value}) and
+  // does NOT include custom option fields like `kind`.  We derive `kind` from
+  // the composite value prefix so consumers can distinguish datasource types.
+  const handleChange = useCallback(
+    (selected: {
+      label: string | ReactNode;
+      value: number | string;
+      kind?: string;
+    }) => {
+      if (typeof selected.value === 'string') {
+        onChange({
+          ...selected,
+          value: fromCompositeValue(selected.value),
+          kind: kindFromComposite(selected.value),
+        });
+      } else {
+        onChange(selected as Parameters<typeof onChange>[0]);
+      }
+    },
+    [onChange],
+  );
+
   return (
     <AsyncSelect
       ariaLabel={datasetLabel()}
-      value={value}
+      value={selectValue}
       options={loadDatasetOptionsCallback}
-      onChange={onChange}
+      onChange={useSemanticLayers ? handleChange : onChange}
       optionFilterProps={['table_name']}
       notFoundContent={t('No compatible %s found', datasetsLabelLower())}
       placeholder={t('Select a %s', datasetLabelLower())}
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 3d28036785e..301d12df571 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -113,6 +113,8 @@ import {
   setNativeFilterFieldValues,
   shouldShowTimeRangePicker,
   useForceUpdate,
+  mapSemanticTypeToGenericDataType,
+  doesChartMatchFilterDatasource,
 } from './utils';
 import {
   CHART_CUSTOMIZATION_SUPPORTED_TYPES,
@@ -407,6 +409,18 @@ const FiltersConfigForm = (
 
   const datasetId = getDatasetId();
 
+  const getDatasourceType = (): string => {
+    if (formFilter?.datasourceType) {
+      return formFilter.datasourceType;
+    }
+    if (isChartCustomization) {
+      return customizationToEdit?.targets?.[0]?.datasourceType || 'table';
+    }
+    return filterToEdit?.targets?.[0]?.datasourceType || 'table';
+  };
+
+  const datasourceType = getDatasourceType();
+
   const formChanged = useCallback(() => {
     form.setFields([
       {
@@ -426,6 +440,7 @@ const FiltersConfigForm = (
     ? getControlItemsMap({
         expanded,
         datasetId,
+        datasourceType,
         disabled: false,
         forceUpdate,
         formChanged,
@@ -497,6 +512,7 @@ const FiltersConfigForm = (
       }
       const formData = getFormData({
         datasetId,
+        datasourceType,
         dashboardId,
         groupby: formFilter?.column,
         ...formFilter,
@@ -567,6 +583,7 @@ const FiltersConfigForm = (
 
   const newFormData = getFormData({
     datasetId,
+    datasourceType,
     groupby: hasColumn ? formFilter?.column : undefined,
     ...formFilter,
   });
@@ -743,45 +760,93 @@ const FiltersConfigForm = (
 
   useEffect(() => {
     if (datasetId) {
-      cachedSupersetGet({
-        endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({
-          columns: [
-            'columns.column_name',
-            'columns.expression',
-            'columns.filterable',
-            'columns.is_dttm',
-            'columns.type',
-            'columns.type_generic',
-            'columns.verbose_name',
-            'database.id',
-            'database.database_name',
-            'datasource_type',
-            'filter_select_enabled',
-            'id',
-            'is_sqllab_view',
-            'main_dttm_col',
-            'metrics.metric_name',
-            'metrics.verbose_name',
-            'schema',
-            'sql',
-            'table_name',
-            'time_grain_sqla',
-          ],
-        })}`,
-      })
-        .then((response: JsonResponse) => {
-          setMetrics(response.json?.result?.metrics);
-          const dataset = response.json?.result;
-          // modify the response to fit structure expected by 
AdhocFilterControl
-          dataset.type = dataset.datasource_type;
-          dataset.filter_select = true;
-          setDatasetDetails(dataset);
+      if (datasourceType === 'semantic_view') {
+        cachedSupersetGet({
+          endpoint: `/api/v1/semantic_view/${datasetId}/structure`,
         })
-        .catch((response: SupersetApiError) => {
-          addDangerToast(response.message);
-        });
+          .then((response: JsonResponse) => {
+            const {
+              name: svName,
+              dimensions = [],
+              metrics: svMetrics = [],
+            } = response.json?.result ?? {};
+            const columns = dimensions.map(
+              (dim: { name: string; type: string }) => {
+                const mappedType = mapSemanticTypeToGenericDataType(dim.type);
+                return {
+                  column_name: dim.name,
+                  type: dim.type,
+                  is_dttm: mappedType === GenericDataType.Temporal,
+                  filterable: true,
+                  type_generic: mappedType,
+                };
+              },
+            );
+            const mappedMetrics = svMetrics.map(
+              (m: { name: string; definition: string }) => ({
+                metric_name: m.name,
+                expression: m.definition,
+                verbose_name: null,
+              }),
+            );
+            setMetrics(mappedMetrics);
+            setDatasetDetails({
+              columns,
+              metrics: mappedMetrics,
+              datasource_type: 'semantic_view',
+              type: 'semantic_view',
+              filter_select: true,
+              filter_select_enabled: true,
+              time_grain_sqla: [],
+              main_dttm_col: null,
+              id: datasetId,
+              table_name: svName,
+            });
+          })
+          .catch((response: SupersetApiError) => {
+            addDangerToast(response.message);
+          });
+      } else {
+        cachedSupersetGet({
+          endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({
+            columns: [
+              'columns.column_name',
+              'columns.expression',
+              'columns.filterable',
+              'columns.is_dttm',
+              'columns.type',
+              'columns.type_generic',
+              'columns.verbose_name',
+              'database.id',
+              'database.database_name',
+              'datasource_type',
+              'filter_select_enabled',
+              'id',
+              'is_sqllab_view',
+              'main_dttm_col',
+              'metrics.metric_name',
+              'metrics.verbose_name',
+              'schema',
+              'sql',
+              'table_name',
+              'time_grain_sqla',
+            ],
+          })}`,
+        })
+          .then((response: JsonResponse) => {
+            setMetrics(response.json?.result?.metrics);
+            const dataset = response.json?.result;
+            // modify the response to fit structure expected by 
AdhocFilterControl
+            dataset.type = dataset.datasource_type;
+            dataset.filter_select = true;
+            setDatasetDetails(dataset);
+          })
+          .catch((response: SupersetApiError) => {
+            addDangerToast(response.message);
+          });
+      }
     }
-  }, [datasetId]);
+  }, [datasetId, datasourceType]);
 
   useImperativeHandle(ref, () => ({
     changeTab(tab: 'configuration' | 'scoping') {
@@ -820,7 +885,15 @@ const FiltersConfigForm = (
       if (chartDatasetUid === undefined) {
         return;
       }
-      if (loadedDatasets[chartDatasetUid]?.id !== formFilter?.dataset?.value) {
+
+      const matchesFilterDatasource = doesChartMatchFilterDatasource(
+        chartDatasetUid,
+        loadedDatasets,
+        formFilter.dataset.value,
+        datasourceType,
+      );
+
+      if (!matchesFilterDatasource) {
         excluded.push(chart.id);
       }
     });
@@ -828,6 +901,7 @@ const FiltersConfigForm = (
   }, [
     JSON.stringify(Object.values(charts).map(chart => chart.id)),
     formFilter?.dataset?.value,
+    datasourceType,
     JSON.stringify(loadedDatasets),
   ]);
 
@@ -876,6 +950,7 @@ const FiltersConfigForm = (
         filterId={filterId}
         filterValues={(column: Column) => !!column.is_dttm}
         datasetId={datasetId}
+        datasourceType={datasourceType}
         onChange={column => {
           // We need reset default value when column changed
           setNativeFilterFieldValues(form, filterId, {
@@ -1064,16 +1139,23 @@ const FiltersConfigForm = (
                         initialValue={
                           datasetDetails
                             ? {
-                                label: DatasetSelectLabel({
-                                  id: datasetDetails.id,
-                                  table_name: datasetDetails.table_name,
-                                  schema: datasetDetails.schema,
-                                  database: {
-                                    database_name:
-                                      datasetDetails.database.database_name,
-                                  },
-                                }),
+                                label: datasetDetails.database
+                                  ? DatasetSelectLabel({
+                                      id: datasetDetails.id,
+                                      table_name: datasetDetails.table_name,
+                                      schema: datasetDetails.schema,
+                                      database: {
+                                        database_name:
+                                          
datasetDetails.database.database_name,
+                                      },
+                                    })
+                                  : (datasetDetails.table_name ??
+                                    datasetDetails.id),
                                 value: datasetDetails.id,
+                                kind:
+                                  datasourceType === 'semantic_view'
+                                    ? 'semantic_view'
+                                    : undefined,
                               }
                             : undefined
                         }
@@ -1092,11 +1174,20 @@ const FiltersConfigForm = (
                           onChange={(value: {
                             label: string | React.ReactNode;
                             value: number;
+                            kind?: string;
                           }) => {
-                            if (value.value !== datasetId) {
+                            const newDatasourceType =
+                              value.kind === 'semantic_view'
+                                ? 'semantic_view'
+                                : 'table';
+                            if (
+                              value.value !== datasetId ||
+                              newDatasourceType !== datasourceType
+                            ) {
                               setNativeFilterFieldValues(form, filterId, {
                                 dataset: value,
                                 datasetInfo: value,
+                                datasourceType: newDatasourceType,
                                 defaultDataMask: null,
                                 column: null,
                               });
@@ -1652,6 +1743,11 @@ const FiltersConfigForm = (
                                 : NativeFilterType.NativeFilter
                             }
                           />
+                          <FormItem
+                            name={['filters', filterId, 'datasourceType']}
+                            hidden
+                            initialValue={datasourceType}
+                          />
                           <FormItem
                             name={[
                               'filters',
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
index 7b8f9577723..83273764460 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
@@ -48,6 +48,7 @@ import { ColumnSelect } from './ColumnSelect';
 export interface ControlItemsProps {
   expanded: boolean;
   datasetId: number;
+  datasourceType?: string;
   disabled: boolean;
   forceUpdate: Function;
   formChanged: Function;
@@ -67,6 +68,7 @@ const CleanFormItem = styled(FormItem)`
 export default function getControlItemsMap({
   expanded,
   datasetId,
+  datasourceType,
   disabled,
   forceUpdate,
   formChanged,
@@ -139,6 +141,7 @@ export default function getControlItemsMap({
               form={form}
               filterId={filterId}
               datasetId={datasetId}
+              datasourceType={datasourceType}
               filterValues={column =>
                 doesColumnMatchFilterType(
                   formFilter?.filterType || '',
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.test.ts
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.test.ts
index 6bfbb52a80d..a616c7c1a39 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.test.ts
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.test.ts
@@ -31,6 +31,8 @@ import {
   mostUsedDataset,
   doesColumnMatchFilterType,
   getTimeGrainOptions,
+  mapSemanticTypeToGenericDataType,
+  doesChartMatchFilterDatasource,
 } from './utils';
 
 // Test hasTemporalColumns - validates time range pre-filter visibility logic
@@ -303,3 +305,75 @@ test('getTimeGrainOptions falls back to value when tuple 
label is empty', () =>
     { value: 'P1W', label: 'Week' },
   ]);
 });
+
+test('mapSemanticTypeToGenericDataType maps numeric semantic types', () => {
+  expect(mapSemanticTypeToGenericDataType('int64')).toBe(
+    GenericDataType.Numeric,
+  );
+  expect(mapSemanticTypeToGenericDataType('decimal128(10,2)')).toBe(
+    GenericDataType.Numeric,
+  );
+});
+
+test('mapSemanticTypeToGenericDataType maps temporal semantic types', () => {
+  expect(mapSemanticTypeToGenericDataType('timestamp[ms]')).toBe(
+    GenericDataType.Temporal,
+  );
+  expect(mapSemanticTypeToGenericDataType('date32[day]')).toBe(
+    GenericDataType.Temporal,
+  );
+});
+
+test('mapSemanticTypeToGenericDataType maps string and boolean semantic 
types', () => {
+  expect(mapSemanticTypeToGenericDataType('string')).toBe(
+    GenericDataType.String,
+  );
+  expect(mapSemanticTypeToGenericDataType('bool')).toBe(
+    GenericDataType.Boolean,
+  );
+});
+
+test('mapSemanticTypeToGenericDataType returns undefined for unknown types', 
() => {
+  expect(mapSemanticTypeToGenericDataType('struct<a:int64>')).toBeUndefined();
+  expect(mapSemanticTypeToGenericDataType(undefined)).toBeUndefined();
+});
+
+test('doesChartMatchFilterDatasource requires matching datasource type for 
equal IDs', () => {
+  const loadedDatasets = {
+    '7__table': { id: 7, datasource_type: 'table' },
+    '7__semantic_view': { id: 7, datasource_type: 'semantic_view' },
+  } as unknown as DatasourcesState;
+
+  expect(
+    doesChartMatchFilterDatasource(
+      '7__table',
+      loadedDatasets,
+      7,
+      'semantic_view',
+    ),
+  ).toBe(false);
+  expect(
+    doesChartMatchFilterDatasource(
+      '7__semantic_view',
+      loadedDatasets,
+      7,
+      'semantic_view',
+    ),
+  ).toBe(true);
+});
+
+test('doesChartMatchFilterDatasource falls back to datasource UID parsing', () 
=> {
+  const loadedDatasets = {} as DatasourcesState;
+
+  expect(
+    doesChartMatchFilterDatasource('7__semantic_view', loadedDatasets, 7),
+  ).toBe(false);
+  expect(
+    doesChartMatchFilterDatasource(
+      '7__semantic_view',
+      loadedDatasets,
+      7,
+      'semantic_view',
+    ),
+  ).toBe(true);
+});
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
index f586dcb5024..0ca302a9ff7 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
@@ -101,6 +101,50 @@ export const doesColumnMatchFilterType = (filterType: 
string, column: Column) =>
     filterType as keyof typeof FILTER_SUPPORTED_TYPES
   ]?.includes(column.type_generic);
 
+export const mapSemanticTypeToGenericDataType = (
+  semanticType?: string | null,
+): GenericDataType | undefined => {
+  if (!semanticType) {
+    return undefined;
+  }
+
+  const normalized = semanticType.toLowerCase();
+
+  if (
+    
/^(struct|list|map|array|fixed_size_list|large_list|union|dictionary)\b/.test(
+      normalized,
+    )
+  ) {
+    return undefined;
+  }
+
+  if (normalized.includes('bool')) {
+    return GenericDataType.Boolean;
+  }
+
+  if (/(date|time|timestamp|datetime)/.test(normalized)) {
+    return GenericDataType.Temporal;
+  }
+
+  if (
+    /(\b(u?int\d*)\b|\bfloat\d*\b|\bdouble\b|\bdecimal\d*\b|\bnumber\b)/.test(
+      normalized,
+    )
+  ) {
+    return GenericDataType.Numeric;
+  }
+
+  if (
+    
/(\bstr(ing)?\b|\butf8\b|\blarge_string\b|\bbinary\b|\bjson\b|\buuid\b)/.test(
+      normalized,
+    )
+  ) {
+    return GenericDataType.String;
+  }
+
+  return undefined;
+};
+
 // Validates that a filter default value is present when the default value 
option is enabled.
 // For range filters, at least one of the two values must be non-null.
 // For other filters (e.g., filter_select), the value must be non-empty.
@@ -144,3 +188,49 @@ export const mostUsedDataset = (
 
   return datasets[mostUsedDataset]?.id;
 };
+
+const normalizeDatasourceType = (datasourceType?: string) =>
+  datasourceType || 'table';
+
+const parseDatasourceUid = (
+  datasourceUid?: string,
+): { id?: number; type?: string } => {
+  if (!datasourceUid) {
+    return {};
+  }
+
+  const [rawId, type] = String(datasourceUid).split('__');
+  const id = Number(rawId);
+  if (Number.isNaN(id)) {
+    return {};
+  }
+
+  return { id, type };
+};
+
+export const doesChartMatchFilterDatasource = (
+  chartDatasourceUid: string | undefined,
+  loadedDatasets: DatasourcesState,
+  filterDatasetId: number,
+  filterDatasourceType?: string,
+): boolean => {
+  const expectedType = normalizeDatasourceType(filterDatasourceType);
+  const loadedDataset = chartDatasourceUid
+    ? loadedDatasets[chartDatasourceUid]
+    : undefined;
+
+  if (loadedDataset) {
+    const loadedType = normalizeDatasourceType(
+      (loadedDataset as unknown as { datasource_type?: string })
+        .datasource_type || loadedDataset.type,
+    );
+
+    return loadedDataset.id === filterDatasetId && loadedType === expectedType;
+  }
+
+  const parsed = parseDatasourceUid(chartDatasourceUid);
+  return (
+    parsed.id === filterDatasetId &&
+    normalizeDatasourceType(parsed.type) === expectedType
+  );
+};
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts
index 2e0eaf3bb49..e32ef05724a 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/customizationTransformer.ts
@@ -92,6 +92,10 @@ function buildCustomizationTarget(
     target.datasetId = formInputs.dataset.value;
   }
 
+  if (formInputs.datasourceType) {
+    target.datasourceType = formInputs.datasourceType;
+  }
+
   if (formInputs.dataset && formInputs.column) {
     target.column = { name: formInputs.column };
   }
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/filterTransformer.ts
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/filterTransformer.ts
index 575d6baebe9..691958244ea 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/filterTransformer.ts
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/transformers/filterTransformer.ts
@@ -97,6 +97,10 @@ function buildFilterTarget(
         : formInputs.dataset;
   }
 
+  if (formInputs.datasourceType) {
+    target.datasourceType = formInputs.datasourceType;
+  }
+
   if (formInputs.dataset && formInputs.column) {
     target.column = { name: formInputs.column };
   }
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
index 78461fcbf20..1e0c1b0914a 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
@@ -54,6 +54,7 @@ export interface NativeFiltersFormItem {
   time_grains?: string[];
   type: typeof NativeFilterType.NativeFilter;
   description: string;
+  datasourceType?: string;
 }
 export interface NativeFilterDivider {
   id: string;
@@ -91,6 +92,7 @@ export interface ChartCustomizationsFormItem {
   granularity_sqla?: string;
   type: typeof NativeFilterType.NativeFilter;
   description: string;
+  datasourceType?: string;
   datasetInfo?: {
     label: string | ReactNode;
     value: number;
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
index 9b3b87641cf..de2ef59c84b 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
@@ -130,6 +130,9 @@ export const createHandleSave =
       if (formInputs.dataset) {
         target.datasetId = formInputs.dataset.value;
       }
+      if (formInputs.datasourceType) {
+        target.datasourceType = formInputs.datasourceType;
+      }
       if (formInputs.dataset && formInputs.column) {
         target.column = { name: formInputs.column };
       }
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts 
b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts
index 4401071eb8f..0044d35f7b2 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts
@@ -46,6 +46,7 @@ const getDefaultRowLimit = (): number => {
 
 export const getFormData = ({
   datasetId,
+  datasourceType,
   dependencies = {},
   groupby,
   defaultDataMask,
@@ -62,6 +63,7 @@ export const getFormData = ({
 }: (Partial<Filter> | Partial<ChartCustomization>) & {
   dashboardId: number;
   datasetId?: number;
+  datasourceType?: string;
   dependencies?: object;
   groupby?: string;
   adhoc_filters?: AdhocFilter[];
@@ -76,7 +78,8 @@ export const getFormData = ({
     sortMetric?: string;
   } = {};
   if (datasetId) {
-    otherProps.datasource = `${datasetId}__table`;
+    const dsType = datasourceType || 'table';
+    otherProps.datasource = `${datasetId}__${dsType}`;
   }
   if (groupby) {
     otherProps.groupby = [groupby];
diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts 
b/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts
index 8bf3e5b774d..b35d3e70a27 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts
+++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.test.ts
@@ -258,7 +258,13 @@ test('CLEAR_ALL_CHART_CUSTOMIZATIONS filters out null 
entries before mapping', (
         null,
         {
           id: 'CUSTOM-1',
-          targets: [{ datasetId: 1, column: { name: 'status' } }],
+          targets: [
+            {
+              datasetId: 1,
+              datasourceType: 'semantic_view',
+              column: { name: 'status' },
+            },
+          ],
           chartsInScope: [10],
         },
         null,
@@ -272,7 +278,9 @@ test('CLEAR_ALL_CHART_CUSTOMIZATIONS filters out null 
entries before mapping', (
   const config = result.metadata?.chart_customization_config;
   expect(config).toHaveLength(1);
   expect(config![0].id).toBe('CUSTOM-1');
-  expect(config![0].targets).toEqual([{ datasetId: 1 }]);
+  expect(config![0].targets).toEqual([
+    { datasetId: 1, datasourceType: 'semantic_view' },
+  ]);
 });
 
 test('CLEAR_ALL_CHART_CUSTOMIZATIONS filters out undefined entries before 
mapping', () => {
@@ -283,7 +291,13 @@ test('CLEAR_ALL_CHART_CUSTOMIZATIONS filters out undefined 
entries before mappin
         undefined,
         {
           id: 'CUSTOM-1',
-          targets: [{ datasetId: 1, column: { name: 'status' } }],
+          targets: [
+            {
+              datasetId: 1,
+              datasourceType: 'semantic_view',
+              column: { name: 'status' },
+            },
+          ],
           chartsInScope: [10],
         },
         undefined,
@@ -297,5 +311,7 @@ test('CLEAR_ALL_CHART_CUSTOMIZATIONS filters out undefined 
entries before mappin
   const config = result.metadata?.chart_customization_config;
   expect(config).toHaveLength(1);
   expect(config![0].id).toBe('CUSTOM-1');
-  expect(config![0].targets).toEqual([{ datasetId: 1 }]);
+  expect(config![0].targets).toEqual([
+    { datasetId: 1, datasourceType: 'semantic_view' },
+  ]);
 });
diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.ts 
b/superset-frontend/src/dashboard/reducers/dashboardInfo.ts
index 26c919f01b3..d2decf48a3f 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardInfo.ts
+++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.ts
@@ -306,6 +306,7 @@ export default function dashboardInfoReducer(
               ...customization,
               targets: customization.targets?.map(target => ({
                 datasetId: target.datasetId,
+                datasourceType: target.datasourceType,
               })),
             }),
           ),
diff --git 
a/superset-frontend/src/features/datasets/DatasetSelectLabel/index.tsx 
b/superset-frontend/src/features/datasets/DatasetSelectLabel/index.tsx
index e1a0c957a5a..f93acd5391e 100644
--- a/superset-frontend/src/features/datasets/DatasetSelectLabel/index.tsx
+++ b/superset-frontend/src/features/datasets/DatasetSelectLabel/index.tsx
@@ -28,6 +28,7 @@ export type Dataset = {
   id: number;
   table_name: string;
   datasource_type?: string;
+  kind?: string;
   schema: string;
   database: Database;
 };
diff --git a/superset/semantic_layers/api.py b/superset/semantic_layers/api.py
index bcb855581db..a5fc9e49d4d 100644
--- a/superset/semantic_layers/api.py
+++ b/superset/semantic_layers/api.py
@@ -260,7 +260,14 @@ class SemanticViewRestApi(BaseSupersetModelRestApi):
             )
             return self.response_422(message=str(ex))
 
-        return self.response(200, result={"dimensions": dimensions, "metrics": 
metrics})
+        return self.response(
+            200,
+            result={
+                "name": view.name,
+                "dimensions": dimensions,
+                "metrics": metrics,
+            },
+        )
 
     @expose("/", methods=("POST",))
     @protect()
diff --git a/superset/semantic_layers/mapper.py 
b/superset/semantic_layers/mapper.py
index b71dfeb44db..cd6cbbf58a8 100644
--- a/superset/semantic_layers/mapper.py
+++ b/superset/semantic_layers/mapper.py
@@ -530,11 +530,11 @@ def _convert_query_object_filter(
     dimension = all_dimensions[col]
 
     val_str = filter_["val"]
-    value: FilterValues | frozenset[FilterValues]
+    value: FilterValues | tuple[FilterValues, ...]
     if val_str is None:
         value = None
     elif isinstance(val_str, (list, tuple)):
-        value = frozenset(val_str)
+        value = tuple(val_str)
     else:
         value = val_str
 
@@ -576,6 +576,8 @@ def _convert_query_object_filter(
         FilterOperator.LESS_THAN_OR_EQUALS.value: Operator.LESS_THAN_OR_EQUAL,
         FilterOperator.IN.value: Operator.IN,
         FilterOperator.NOT_IN.value: Operator.NOT_IN,
+        FilterOperator.ILIKE.value: Operator.LIKE,
+        FilterOperator.NOT_ILIKE.value: Operator.NOT_LIKE,
         FilterOperator.LIKE.value: Operator.LIKE,
         FilterOperator.NOT_LIKE.value: Operator.NOT_LIKE,
         FilterOperator.IS_NULL.value: Operator.IS_NULL,
diff --git a/tests/unit_tests/semantic_layers/mapper_test.py 
b/tests/unit_tests/semantic_layers/mapper_test.py
index 0cbb59c8430..469ad83e670 100644
--- a/tests/unit_tests/semantic_layers/mapper_test.py
+++ b/tests/unit_tests/semantic_layers/mapper_test.py
@@ -368,7 +368,32 @@ def test_convert_query_object_filter_in(mock_datasource: 
MagicMock) -> None:
             type=PredicateType.WHERE,
             column=all_dimensions["category"],
             operator=Operator.IN,
-            value=frozenset({"Electronics", "Books"}),
+            value=("Electronics", "Books"),
+        )
+    }
+
+
+def test_convert_query_object_filter_ilike(mock_datasource: MagicMock) -> None:
+    """
+    Test conversion of ILIKE filter.
+    """
+    all_dimensions = {
+        dim.name: dim for dim in mock_datasource.implementation.dimensions
+    }
+    filter_: ValidatedQueryObjectFilterClause = {
+        "op": FilterOperator.ILIKE.value,
+        "col": "category",
+        "val": "%book%",
+    }
+
+    result = _convert_query_object_filter(filter_, all_dimensions)
+
+    assert result == {
+        Filter(
+            type=PredicateType.WHERE,
+            column=all_dimensions["category"],
+            operator=Operator.LIKE,
+            value="%book%",
         )
     }
 


Reply via email to