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

beto pushed a commit to branch semantic-layer-ui-semantic-view
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 33225aff39b3d2b60d0f839e066806deec203cc6
Author: Beto Dealmeida <[email protected]>
AuthorDate: Wed Feb 11 11:19:50 2026 -0500

    feat: UI for semantic views
---
 docs/static/feature-flags.json                     |   6 +
 .../components/Label/reusable/DatasetTypeLabel.tsx |  20 +-
 .../superset-ui-core/src/utils/featureFlags.ts     |   1 +
 .../semanticViews/SemanticViewEditModal.tsx        | 120 +++++++++
 superset-frontend/src/pages/DatasetList/index.tsx  | 223 +++++++++++++++--
 superset/config.py                                 |   3 +
 superset/datasource/api.py                         | 267 ++++++++++++++++++++-
 superset/semantic_layers/models.py                 |   8 +
 8 files changed, 620 insertions(+), 28 deletions(-)

diff --git a/docs/static/feature-flags.json b/docs/static/feature-flags.json
index 227d529c1db..699cad9e324 100644
--- a/docs/static/feature-flags.json
+++ b/docs/static/feature-flags.json
@@ -69,6 +69,12 @@
         "lifecycle": "development",
         "description": "Expand nested types in Presto into extra 
columns/arrays. Experimental, doesn't work with all nested types."
       },
+      {
+        "name": "SEMANTIC_LAYERS",
+        "default": false,
+        "lifecycle": "development",
+        "description": "Enable semantic layers and show semantic views 
alongside datasets"
+      },
       {
         "name": "TABLE_V2_TIME_COMPARISON_ENABLED",
         "default": false,
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx
index d8567d93b2a..f3d19617742 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx
@@ -23,7 +23,7 @@ import { Label } from '..';
 
 // Define the prop types for DatasetTypeLabel
 interface DatasetTypeLabelProps {
-  datasetType: 'physical' | 'virtual'; // Accepts only 'physical' or 'virtual'
+  datasetType: 'physical' | 'virtual' | 'semantic_view';
 }
 
 const SIZE = 's'; // Define the size as a constant
@@ -32,6 +32,24 @@ export const DatasetTypeLabel: 
React.FC<DatasetTypeLabelProps> = ({
   datasetType,
 }) => {
   const theme = useTheme();
+
+  if (datasetType === 'semantic_view') {
+    return (
+      <Label
+        icon={
+          <Icons.ApartmentOutlined
+            iconSize={SIZE}
+            iconColor={theme.colorInfo}
+          />
+        }
+        type="info"
+        style={{ color: theme.colorInfo }}
+      >
+        {t('Semantic')}
+      </Label>
+    );
+  }
+
   const label: string =
     datasetType === 'physical' ? t('Physical') : t('Virtual');
   const icon =
diff --git 
a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts 
b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index 9770951342b..e117e097599 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -59,6 +59,7 @@ export enum FeatureFlag {
   ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
   Matrixify = 'MATRIXIFY',
   ScheduledQueries = 'SCHEDULED_QUERIES',
+  SemanticLayers = 'SEMANTIC_LAYERS',
   SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
   SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
   SshTunneling = 'SSH_TUNNELING',
diff --git 
a/superset-frontend/src/features/semanticViews/SemanticViewEditModal.tsx 
b/superset-frontend/src/features/semanticViews/SemanticViewEditModal.tsx
new file mode 100644
index 00000000000..ed90d165b9c
--- /dev/null
+++ b/superset-frontend/src/features/semanticViews/SemanticViewEditModal.tsx
@@ -0,0 +1,120 @@
+/**
+ * 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 { useState, useEffect } from 'react';
+import { t } from '@apache-superset/core';
+import { styled } from '@apache-superset/core/ui';
+import { SupersetClient } from '@superset-ui/core';
+import { Input, InputNumber } from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import {
+  StandardModal,
+  ModalFormField,
+  MODAL_STANDARD_WIDTH,
+} from 'src/components/Modal';
+
+const ModalContent = styled.div`
+  padding: ${({ theme }) => theme.sizeUnit * 4}px;
+`;
+
+interface SemanticViewEditModalProps {
+  show: boolean;
+  onHide: () => void;
+  onSave: () => void;
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+  semanticView: {
+    id: number;
+    table_name: string;
+    description?: string | null;
+    cache_timeout?: number | null;
+  } | null;
+}
+
+export default function SemanticViewEditModal({
+  show,
+  onHide,
+  onSave,
+  addDangerToast,
+  addSuccessToast,
+  semanticView,
+}: SemanticViewEditModalProps) {
+  const [description, setDescription] = useState<string>('');
+  const [cacheTimeout, setCacheTimeout] = useState<number | null>(null);
+  const [saving, setSaving] = useState(false);
+
+  useEffect(() => {
+    if (semanticView) {
+      setDescription(semanticView.description || '');
+      setCacheTimeout(semanticView.cache_timeout ?? null);
+    }
+  }, [semanticView]);
+
+  const handleSave = async () => {
+    if (!semanticView) return;
+    setSaving(true);
+    try {
+      await SupersetClient.put({
+        endpoint: `/api/v1/semantic_view/${semanticView.id}`,
+        jsonPayload: {
+          description: description || null,
+          cache_timeout: cacheTimeout,
+        },
+      });
+      addSuccessToast(t('Semantic view updated'));
+      onSave();
+      onHide();
+    } catch {
+      addDangerToast(t('An error occurred while saving the semantic view'));
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  return (
+    <StandardModal
+      show={show}
+      onHide={onHide}
+      onSave={handleSave}
+      title={t('Edit %s', semanticView?.table_name || '')}
+      icon={<Icons.EditOutlined />}
+      isEditMode
+      width={MODAL_STANDARD_WIDTH}
+      saveLoading={saving}
+    >
+      <ModalContent>
+        <ModalFormField label={t('Description')}>
+          <Input.TextArea
+            value={description}
+            onChange={e => setDescription(e.target.value)}
+            rows={4}
+          />
+        </ModalFormField>
+        <ModalFormField label={t('Cache timeout')}>
+          <InputNumber
+            value={cacheTimeout}
+            onChange={value => setCacheTimeout(value as number | null)}
+            min={0}
+            placeholder={t('Duration in seconds')}
+            style={{ width: '100%' }}
+          />
+        </ModalFormField>
+      </ModalContent>
+    </StandardModal>
+  );
+}
diff --git a/superset-frontend/src/pages/DatasetList/index.tsx 
b/superset-frontend/src/pages/DatasetList/index.tsx
index 39684dd9ca7..9ee9bb4fd84 100644
--- a/superset-frontend/src/pages/DatasetList/index.tsx
+++ b/superset-frontend/src/pages/DatasetList/index.tsx
@@ -17,7 +17,12 @@
  * under the License.
  */
 import { t } from '@apache-superset/core';
-import { getExtensionsRegistry, SupersetClient } from '@superset-ui/core';
+import {
+  getExtensionsRegistry,
+  SupersetClient,
+  isFeatureEnabled,
+  FeatureFlag,
+} from '@superset-ui/core';
 import { styled, useTheme, css } from '@apache-superset/core/ui';
 import { FunctionComponent, useState, useMemo, useCallback, Key } from 'react';
 import { Link, useHistory } from 'react-router-dom';
@@ -50,6 +55,7 @@ import {
   ListViewFilterOperator as FilterOperator,
   type ListViewProps,
   type ListViewFilters,
+  type ListViewFetchDataConfig,
 } from 'src/components';
 import { Typography } from '@superset-ui/core/components/Typography';
 import handleResourceExport from 'src/utils/export';
@@ -67,6 +73,7 @@ import {
   CONFIRM_OVERWRITE_MESSAGE,
 } from 'src/features/datasets/constants';
 import DuplicateDatasetModal from 
'src/features/datasets/DuplicateDatasetModal';
+import SemanticViewEditModal from 
'src/features/semanticViews/SemanticViewEditModal';
 import { useSelector } from 'react-redux';
 import { QueryObjectColumns } from 'src/views/CRUD/types';
 import { WIDER_DROPDOWN_WIDTH } from 'src/components/ListView/utils';
@@ -120,13 +127,16 @@ type Dataset = {
   database: {
     id: string;
     database_name: string;
-  };
+  } | null;
   kind: string;
+  source_type?: 'database' | 'semantic_layer';
   explore_url: string;
   id: number;
   owners: Array<Owner>;
   schema: string;
   table_name: string;
+  description?: string | null;
+  cache_timeout?: number | null;
 };
 
 interface VirtualDataset extends Dataset {
@@ -152,18 +162,90 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
   const history = useHistory();
   const theme = useTheme();
   const {
-    state: {
-      loading,
-      resourceCount: datasetCount,
-      resourceCollection: datasets,
-      bulkSelectEnabled,
-    },
+    state: { bulkSelectEnabled },
     hasPerm,
-    fetchData,
     toggleBulkSelect,
-    refreshData,
   } = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast);
 
+  // Combined endpoint state
+  const [datasets, setDatasets] = useState<Dataset[]>([]);
+  const [datasetCount, setDatasetCount] = useState(0);
+  const [loading, setLoading] = useState(true);
+  const [lastFetchConfig, setLastFetchConfig] =
+    useState<ListViewFetchDataConfig | null>(null);
+  const [currentSourceFilter, setCurrentSourceFilter] = useState<string>('');
+
+  const fetchData = useCallback((config: ListViewFetchDataConfig) => {
+    setLastFetchConfig(config);
+    setLoading(true);
+    const { pageIndex, pageSize, sortBy, filters: filterValues } = config;
+
+    // Separate source_type filter from other filters
+    const sourceTypeFilter = filterValues.find(f => f.id === 'source_type');
+
+    // Track source filter for conditional Type filter visibility
+    const sourceVal =
+      sourceTypeFilter?.value && typeof sourceTypeFilter.value === 'object'
+        ? (sourceTypeFilter.value as { value: string }).value
+        : ((sourceTypeFilter?.value as string) ?? '');
+    setCurrentSourceFilter(sourceVal);
+    const otherFilters = filterValues
+      .filter(f => f.id !== 'source_type')
+      .filter(
+        ({ value }) => value !== '' && value !== null && value !== undefined,
+      )
+      .map(({ id, operator: opr, value }) => ({
+        col: id,
+        opr,
+        value:
+          value && typeof value === 'object' && 'value' in value
+            ? value.value
+            : value,
+      }));
+
+    // Add source_type filter for the combined endpoint
+    const sourceTypeValue =
+      sourceTypeFilter?.value && typeof sourceTypeFilter.value === 'object'
+        ? (sourceTypeFilter.value as { value: string }).value
+        : sourceTypeFilter?.value;
+    if (sourceTypeValue) {
+      otherFilters.push({
+        col: 'source_type',
+        opr: 'eq',
+        value: sourceTypeValue,
+      });
+    }
+
+    const queryParams = rison.encode_uri({
+      order_column: sortBy[0].id,
+      order_direction: sortBy[0].desc ? 'desc' : 'asc',
+      page: pageIndex,
+      page_size: pageSize,
+      ...(otherFilters.length ? { filters: otherFilters } : {}),
+    });
+
+    return SupersetClient.get({
+      endpoint: `/api/v1/datasource/?q=${queryParams}`,
+    })
+      .then(({ json = {} }) => {
+        setDatasets(json.result);
+        setDatasetCount(json.count);
+      })
+      .catch(() => {
+        addDangerToast(t('An error occurred while fetching datasets'));
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }, []);
+
+  const refreshData = useCallback(() => {
+    if (lastFetchConfig) {
+      return fetchData(lastFetchConfig);
+    }
+    return undefined;
+  }, [lastFetchConfig, fetchData]);
+
   const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
     | (Dataset & {
         charts: any;
@@ -178,6 +260,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
   const [datasetCurrentlyDuplicating, setDatasetCurrentlyDuplicating] =
     useState<VirtualDataset | null>(null);
 
+  const [svCurrentlyEditing, setSvCurrentlyEditing] = useState<Dataset | null>(
+    null,
+  );
+
   const [importingDataset, showImportModal] = useState<boolean>(false);
   const [passwordFields, setPasswordFields] = useState<string[]>([]);
   const [preparingExport, setPreparingExport] = useState<boolean>(false);
@@ -372,12 +458,22 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
         id: 'kind',
       },
       {
+        Cell: ({
+          row: {
+            original: { database },
+          },
+        }: any) => database?.database_name || '-',
         Header: t('Database'),
         accessor: 'database.database_name',
         size: 'xl',
         id: 'database.database_name',
       },
       {
+        Cell: ({
+          row: {
+            original: { schema },
+          },
+        }: any) => schema || '-',
         Header: t('Schema'),
         accessor: 'schema',
         size: 'lg',
@@ -420,9 +516,40 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
         disableSortBy: true,
         id: 'sql',
       },
+      {
+        accessor: 'source_type',
+        hidden: true,
+        disableSortBy: true,
+        id: 'source_type',
+      },
       {
         Cell: ({ row: { original } }: any) => {
-          // Verify owner or isAdmin
+          const isSemanticView = original.source_type === 'semantic_layer';
+
+          // Semantic view: only show edit button
+          if (isSemanticView) {
+            if (!canEdit) return null;
+            return (
+              <Actions className="actions">
+                <Tooltip
+                  id="edit-action-tooltip"
+                  title={t('Edit')}
+                  placement="bottom"
+                >
+                  <span
+                    role="button"
+                    tabIndex={0}
+                    className="action-button"
+                    onClick={() => setSvCurrentlyEditing(original)}
+                  >
+                    <Icons.EditOutlined iconSize="l" />
+                  </span>
+                </Tooltip>
+              </Actions>
+            );
+          }
+
+          // Dataset: full set of actions
           const allowEdit =
             original.owners.map((o: Owner) => o.id).includes(user.userId) ||
             isUserAdmin(user);
@@ -536,6 +663,22 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
 
   const filterTypes: ListViewFilters = useMemo(
     () => [
+      ...(isFeatureEnabled(FeatureFlag.SemanticLayers)
+        ? [
+            {
+              Header: t('Source'),
+              key: 'source_type',
+              id: 'source_type',
+              input: 'select' as const,
+              operator: FilterOperator.Equals,
+              unfilteredLabel: t('All'),
+              selects: [
+                { label: t('Database'), value: 'database' },
+                { label: t('Semantic Layer'), value: 'semantic_layer' },
+              ],
+            },
+          ]
+        : []),
       {
         Header: t('Name'),
         key: 'search',
@@ -543,18 +686,42 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
         input: 'search',
         operator: FilterOperator.Contains,
       },
-      {
-        Header: t('Type'),
-        key: 'sql',
-        id: 'sql',
-        input: 'select',
-        operator: FilterOperator.DatasetIsNullOrEmpty,
-        unfilteredLabel: 'All',
-        selects: [
-          { label: t('Virtual'), value: false },
-          { label: t('Physical'), value: true },
-        ],
-      },
+      ...(isFeatureEnabled(FeatureFlag.SemanticLayers)
+        ? [
+            {
+              Header: t('Type'),
+              key: 'sql',
+              id: 'sql',
+              input: 'select' as const,
+              operator: FilterOperator.DatasetIsNullOrEmpty,
+              unfilteredLabel: 'All',
+              selects: [
+                ...(currentSourceFilter !== 'semantic_layer'
+                  ? [
+                      { label: t('Physical'), value: true },
+                      { label: t('Virtual'), value: false },
+                    ]
+                  : []),
+                ...(currentSourceFilter !== 'database'
+                  ? [{ label: t('Semantic View'), value: 'semantic_view' }]
+                  : []),
+              ],
+            },
+          ]
+        : [
+            {
+              Header: t('Type'),
+              key: 'sql',
+              id: 'sql',
+              input: 'select' as const,
+              operator: FilterOperator.DatasetIsNullOrEmpty,
+              unfilteredLabel: 'All',
+              selects: [
+                { label: t('Physical'), value: true },
+                { label: t('Virtual'), value: false },
+              ],
+            },
+          ]),
       {
         Header: t('Database'),
         key: 'database',
@@ -645,7 +812,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
         dropdownStyle: { minWidth: WIDER_DROPDOWN_WIDTH },
       },
     ],
-    [user],
+    [user, currentSourceFilter],
   );
 
   const menuData: SubMenuProps = {
@@ -897,6 +1064,14 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
         onHide={closeDatasetDuplicateModal}
         onDuplicate={handleDatasetDuplicate}
       />
+      <SemanticViewEditModal
+        show={!!svCurrentlyEditing}
+        onHide={() => setSvCurrentlyEditing(null)}
+        onSave={refreshData}
+        addDangerToast={addDangerToast}
+        addSuccessToast={addSuccessToast}
+        semanticView={svCurrentlyEditing}
+      />
       <ConfirmStatusChange
         title={t('Please confirm')}
         description={t(
diff --git a/superset/config.py b/superset/config.py
index dfeafb3b261..fe1d5da2fa9 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -562,6 +562,9 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
     # in addition to relative timeshifts (e.g., "1 day ago")
     # @lifecycle: development
     "DATE_RANGE_TIMESHIFTS_ENABLED": False,
+    # Enable semantic layers and show semantic views alongside datasets
+    # @lifecycle: development
+    "SEMANTIC_LAYERS": False,
     # Enables advanced data type support
     # @lifecycle: development
     "ENABLE_ADVANCED_DATA_TYPES": False,
diff --git a/superset/datasource/api.py b/superset/datasource/api.py
index da5f7e3ded8..ca8a235192b 100644
--- a/superset/datasource/api.py
+++ b/superset/datasource/api.py
@@ -15,17 +15,23 @@
 # specific language governing permissions and limitations
 # under the License.
 import logging
+from typing import Any
 
 from flask import current_app as app, request
-from flask_appbuilder.api import expose, protect, safe
+from flask_appbuilder.api import expose, protect, rison, safe
+from flask_appbuilder.api.schemas import get_list_schema
+from sqlalchemy import and_, func, literal, or_, select, union_all
 
-from superset import event_logger
-from superset.connectors.sqla.models import BaseDatasource
+from superset import db, event_logger, is_feature_enabled, security_manager
+from superset.connectors.sqla import models as sqla_models
+from superset.connectors.sqla.models import BaseDatasource, SqlaTable
 from superset.daos.datasource import DatasourceDAO
 from superset.daos.exceptions import DatasourceNotFound, 
DatasourceTypeNotSupportedError
 from superset.exceptions import SupersetSecurityException
+from superset.semantic_layers.models import SemanticView
 from superset.superset_typing import FlaskResponse
 from superset.utils.core import apply_max_row_limit, DatasourceType, 
SqlExpressionType
+from superset.utils.filters import get_dataset_access_filters
 from superset.views.base_api import BaseSupersetApi, statsd_metrics
 
 logger = logging.getLogger(__name__)
@@ -303,3 +309,258 @@ class DatasourceRestApi(BaseSupersetApi):
                 f"Invalid expression type: {expression_type}. "
                 f"Valid types are: column, metric, where, having"
             ) from None
+
+    @expose("/", methods=("GET",))
+    @safe
+    @statsd_metrics
+    @rison(get_list_schema)
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
+        ".combined_list",
+        log_to_statsd=False,
+    )
+    def combined_list(self, **kwargs: Any) -> FlaskResponse:
+        """List datasets and semantic views combined.
+        ---
+        get:
+          summary: List datasets and semantic views combined
+          parameters:
+          - in: query
+            name: q
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/get_list_schema'
+          responses:
+            200:
+              description: Combined list of datasets and semantic views
+            401:
+              $ref: '#/components/responses/401'
+            403:
+              $ref: '#/components/responses/403'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        if not security_manager.can_access("can_read", "Dataset"):
+            return self.response(403, message="Access denied")
+
+        args = kwargs.get("rison", {})
+        page = args.get("page", 0)
+        page_size = args.get("page_size", 25)
+        order_column = args.get("order_column", "changed_on")
+        order_direction = args.get("order_direction", "desc")
+        filters = args.get("filters", [])
+
+        # Extract source_type, name search, sql (type), and type_filter
+        source_type = "all"
+        name_filter = None
+        sql_filter = None  # None = no filter, True = physical, False = virtual
+        type_filter = None  # None = no filter, "semantic_view" / True / False
+        for f in filters:
+            if f.get("col") == "source_type":
+                source_type = f.get("value", "all")
+            elif f.get("col") == "table_name" and f.get("opr") == "ct":
+                name_filter = f.get("value")
+            elif f.get("col") == "sql":
+                val = f.get("value")
+                if val == "semantic_view":
+                    type_filter = "semantic_view"
+                else:
+                    sql_filter = val
+
+        # If semantic layers feature flag is off, only show datasets
+        if not is_feature_enabled("SEMANTIC_LAYERS"):
+            source_type = "database"
+
+        # Map sort columns
+        sort_col_map = {
+            "changed_on_delta_humanized": "changed_on",
+            "table_name": "table_name",
+        }
+        sort_col_name = sort_col_map.get(order_column, "changed_on")
+
+        # Build dataset subquery
+        ds_q = select(
+            SqlaTable.id.label("item_id"),
+            literal("database").label("source_type"),
+            SqlaTable.changed_on,
+            SqlaTable.table_name,
+        ).select_from(SqlaTable.__table__)
+
+        # Apply security filters for datasets
+        if not security_manager.can_access_all_datasources():
+            ds_q = ds_q.join(
+                sqla_models.Database,
+                sqla_models.Database.id == SqlaTable.database_id,
+            )
+            ds_q = ds_q.where(get_dataset_access_filters(SqlaTable))
+
+        # Apply name filter to datasets
+        if name_filter:
+            ds_q = ds_q.where(SqlaTable.table_name.ilike(f"%{name_filter}%"))
+
+        # Apply sql (type) filter to datasets
+        if sql_filter is not None:
+            if sql_filter:
+                # Physical: sql is null or empty
+                ds_q = ds_q.where(
+                    or_(SqlaTable.sql.is_(None), SqlaTable.sql == "")
+                )
+            else:
+                # Virtual: sql is not null and not empty
+                ds_q = ds_q.where(
+                    and_(SqlaTable.sql.isnot(None), SqlaTable.sql != "")
+                )
+            # Selecting Physical/Virtual implicitly means "database only"
+            if source_type == "all":
+                source_type = "database"
+
+        # Handle type_filter = "semantic_view"
+        if type_filter == "semantic_view":
+            source_type = "semantic_layer"
+
+        # Build semantic view subquery
+        sv_q = select(
+            SemanticView.id.label("item_id"),
+            literal("semantic_layer").label("source_type"),
+            SemanticView.changed_on,
+            SemanticView.name.label("table_name"),
+        ).select_from(SemanticView.__table__)
+
+        # Apply name filter to semantic views
+        if name_filter:
+            sv_q = sv_q.where(SemanticView.name.ilike(f"%{name_filter}%"))
+
+        # Build combined query based on source_type
+        if source_type == "database":
+            combined = ds_q.subquery()
+        elif source_type == "semantic_layer":
+            combined = sv_q.subquery()
+        else:
+            combined = union_all(ds_q, sv_q).subquery()
+
+        # Count total
+        count_q = select(func.count()).select_from(combined)
+        total_count = db.session.execute(count_q).scalar() or 0
+
+        # Sort and paginate
+        sort_col = combined.c[sort_col_name]
+        if order_direction == "desc":
+            sort_col = sort_col.desc()
+        else:
+            sort_col = sort_col.asc()
+
+        paginated_q = (
+            select(
+                combined.c.item_id,
+                combined.c.source_type,
+            )
+            .order_by(sort_col)
+            .offset(page * page_size)
+            .limit(page_size)
+        )
+        rows = db.session.execute(paginated_q).fetchall()
+
+        # Collect IDs by type
+        dataset_ids = [r.item_id for r in rows if r.source_type == "database"]
+        sv_ids = [r.item_id for r in rows if r.source_type == "semantic_layer"]
+
+        # Fetch full ORM objects
+        datasets_map: dict[int, SqlaTable] = {}
+        if dataset_ids:
+            ds_objs = (
+                db.session.query(SqlaTable)
+                .filter(SqlaTable.id.in_(dataset_ids))
+                .all()
+            )
+            datasets_map = {obj.id: obj for obj in ds_objs}
+
+        sv_map: dict[int, SemanticView] = {}
+        if sv_ids:
+            sv_objs = (
+                db.session.query(SemanticView)
+                .filter(SemanticView.id.in_(sv_ids))
+                .all()
+            )
+            sv_map = {obj.id: obj for obj in sv_objs}
+
+        # Serialize in UNION order
+        result = []
+        for row in rows:
+            if row.source_type == "database":
+                obj = datasets_map.get(row.item_id)
+                if obj:
+                    result.append(self._serialize_dataset(obj))
+            else:
+                obj = sv_map.get(row.item_id)
+                if obj:
+                    result.append(self._serialize_semantic_view(obj))
+
+        return self.response(200, count=total_count, result=result)
+
+    @staticmethod
+    def _serialize_dataset(obj: SqlaTable) -> dict[str, Any]:
+        changed_by = obj.changed_by
+        return {
+            "id": obj.id,
+            "uuid": str(obj.uuid),
+            "table_name": obj.table_name,
+            "kind": obj.kind,
+            "source_type": "database",
+            "description": obj.description,
+            "explore_url": obj.explore_url,
+            "database": {
+                "id": obj.database_id,
+                "database_name": obj.database.database_name,
+            }
+            if obj.database
+            else None,
+            "schema": obj.schema,
+            "sql": obj.sql,
+            "extra": obj.extra,
+            "owners": [
+                {
+                    "id": o.id,
+                    "first_name": o.first_name,
+                    "last_name": o.last_name,
+                }
+                for o in obj.owners
+            ],
+            "changed_by_name": obj.changed_by_name,
+            "changed_by": {
+                "first_name": changed_by.first_name,
+                "last_name": changed_by.last_name,
+            }
+            if changed_by
+            else None,
+            "changed_on_delta_humanized": obj.changed_on_delta_humanized(),
+            "changed_on_utc": obj.changed_on_utc(),
+        }
+
+    @staticmethod
+    def _serialize_semantic_view(obj: SemanticView) -> dict[str, Any]:
+        changed_by = obj.changed_by
+        return {
+            "id": obj.id,
+            "uuid": str(obj.uuid),
+            "table_name": obj.name,
+            "kind": "semantic_view",
+            "source_type": "semantic_layer",
+            "description": obj.description,
+            "cache_timeout": obj.cache_timeout,
+            "explore_url": obj.explore_url,
+            "database": None,
+            "schema": None,
+            "sql": None,
+            "extra": None,
+            "owners": [],
+            "changed_by_name": obj.changed_by_name,
+            "changed_by": {
+                "first_name": changed_by.first_name,
+                "last_name": changed_by.last_name,
+            }
+            if changed_by
+            else None,
+            "changed_on_delta_humanized": obj.changed_on_delta_humanized(),
+            "changed_on_utc": obj.changed_on_utc(),
+        }
diff --git a/superset/semantic_layers/models.py 
b/superset/semantic_layers/models.py
index 80c0d88d400..95b225e9592 100644
--- a/superset/semantic_layers/models.py
+++ b/superset/semantic_layers/models.py
@@ -205,6 +205,14 @@ class SemanticView(AuditMixinNullable, Model):
     def get_query_str(self, query_obj: QueryObjectDict) -> str:
         return "Not implemented for semantic layers"
 
+    @property
+    def table_name(self) -> str:
+        return self.name
+
+    @property
+    def kind(self) -> str:
+        return "semantic_view"
+
     @property
     def uid(self) -> str:
         return self.implementation.uid()

Reply via email to