You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by hu...@apache.org on 2023/06/21 15:43:20 UTC

[superset] 01/01: fix merge conflicts

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

hugh pushed a commit to branch 2023.25.1
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 249c05262ed0b8c0239c95043971358c21942eb7
Author: hughhhh <hu...@gmail.com>
AuthorDate: Wed Jun 21 11:42:24 2023 -0400

    fix merge conflicts
---
 superset-frontend/src/SqlLab/actions/sqlLab.js     |  15 +-
 .../SqlLab/components/SaveDatasetModal/index.tsx   |   6 +-
 .../src/explore/actions/datasourcesActions.test.ts |   2 +-
 .../src/explore/actions/datasourcesActions.ts      |  42 ++---
 superset/views/core.py                             | 187 ++++++++++++++++++++-
 5 files changed, 214 insertions(+), 38 deletions(-)

diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 708f9a3346..d14d1de211 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -1432,7 +1432,7 @@ export function createDatasourceStarted() {
   return { type: CREATE_DATASOURCE_STARTED };
 }
 export function createDatasourceSuccess(data) {
-  const datasource = `${data.id}__table`;
+  const datasource = `${data.table_id}__table`;
   return { type: CREATE_DATASOURCE_SUCCESS, datasource };
 }
 export function createDatasourceFailed(err) {
@@ -1442,18 +1442,9 @@ export function createDatasourceFailed(err) {
 export function createDatasource(vizOptions) {
   return dispatch => {
     dispatch(createDatasourceStarted());
-    const { dbId, schema, datasourceName, sql } = vizOptions;
     return SupersetClient.post({
-      endpoint: '/api/v1/dataset/',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify({
-        database: dbId,
-        schema,
-        sql,
-        table_name: datasourceName,
-        is_managed_externally: false,
-        external_url: null,
-      }),
+      endpoint: '/superset/sqllab_viz/',
+      postPayload: { data: vizOptions },
     })
       .then(({ json }) => {
         dispatch(createDatasourceSuccess(json));
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
index c51c6158d8..b1ab18eb09 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
@@ -300,10 +300,10 @@ export const SaveDatasetModal = ({
         columns: selectedColumns,
       }),
     )
-      .then((data: { id: number }) =>
-        postFormData(data.id, 'table', {
+      .then((data: { table_id: number }) =>
+        postFormData(data.table_id, 'table', {
           ...formDataWithDefaults,
-          datasource: `${data.id}__table`,
+          datasource: `${data.table_id}__table`,
           ...(defaultVizType === 'table' && {
             all_columns: selectedColumns.map(column => column.column_name),
           }),
diff --git a/superset-frontend/src/explore/actions/datasourcesActions.test.ts b/superset-frontend/src/explore/actions/datasourcesActions.test.ts
index bca3aecfd6..996758b262 100644
--- a/superset-frontend/src/explore/actions/datasourcesActions.test.ts
+++ b/superset-frontend/src/explore/actions/datasourcesActions.test.ts
@@ -68,7 +68,7 @@ const defaultDatasourcesReducerState = {
   [CURRENT_DATASOURCE.uid]: CURRENT_DATASOURCE,
 };
 
-const saveDatasetEndpoint = `glob:*/api/v1/dataset/`;
+const saveDatasetEndpoint = `glob:*/superset/sqllab_viz/`;
 
 test('sets new datasource', () => {
   const newState = datasourcesReducer(
diff --git a/superset-frontend/src/explore/actions/datasourcesActions.ts b/superset-frontend/src/explore/actions/datasourcesActions.ts
index c11be07be3..9306c180e2 100644
--- a/superset-frontend/src/explore/actions/datasourcesActions.ts
+++ b/superset-frontend/src/explore/actions/datasourcesActions.ts
@@ -35,16 +35,6 @@ export function setDatasource(datasource: Dataset) {
   return { type: SET_DATASOURCE, datasource };
 }
 
-export function changeDatasource(newDatasource: Dataset) {
-  return function (dispatch: Dispatch, getState: () => ExplorePageState) {
-    const {
-      explore: { datasource: prevDatasource },
-    } = getState();
-    dispatch(setDatasource(newDatasource));
-    dispatch(updateFormDataByDatasource(prevDatasource, newDatasource));
-  };
-}
-
 export function saveDataset({
   schema,
   sql,
@@ -59,16 +49,18 @@ export function saveDataset({
       const {
         json: { data },
       } = await SupersetClient.post({
-        endpoint: '/api/v1/dataset/',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          database: database?.id,
-          table_name: datasourceName,
-          schema,
-          sql,
-          template_params: templateParams,
-          columns,
-        }),
+        endpoint: '/superset/sqllab_viz/',
+        postPayload: {
+          data: {
+            schema,
+            sql,
+            dbId: database?.id,
+            templateParams,
+            datasourceName,
+            metrics: [],
+            columns,
+          },
+        },
       });
       // Update form_data to point to new dataset
       dispatch(changeDatasource(data));
@@ -82,6 +74,16 @@ export function saveDataset({
   };
 }
 
+export function changeDatasource(newDatasource: Dataset) {
+  return function (dispatch: Dispatch, getState: () => ExplorePageState) {
+    const {
+      explore: { datasource: prevDatasource },
+    } = getState();
+    dispatch(setDatasource(newDatasource));
+    dispatch(updateFormDataByDatasource(prevDatasource, newDatasource));
+  };
+}
+
 export const datasourcesActions = {
   setDatasource,
   changeDatasource,
diff --git a/superset/views/core.py b/superset/views/core.py
index f75a8f77e1..a61575345b 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -46,7 +46,7 @@ from superset.charts.commands.exceptions import ChartNotFoundError
 from superset.charts.dao import ChartDAO
 from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
 from superset.connectors.base.models import BaseDatasource
-from superset.connectors.sqla.models import SqlaTable
+from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
 from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
 from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand
 from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand
@@ -54,18 +54,28 @@ from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailed
 from superset.databases.dao import DatabaseDAO
 from superset.datasets.commands.exceptions import DatasetNotFoundError
 from superset.datasource.dao import DatasourceDAO
-from superset.exceptions import CacheLoadError, DatabaseNotFound, SupersetException
+from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
+from superset.exceptions import (
+    CacheLoadError,
+    DatabaseNotFound,
+    SupersetErrorException,
+    SupersetException,
+    SupersetGenericErrorException,
+    SupersetTimeoutException,
+)
 from superset.explore.form_data.commands.create import CreateFormDataCommand
 from superset.explore.form_data.commands.get import GetFormDataCommand
 from superset.explore.form_data.commands.parameters import CommandParameters
 from superset.explore.permalink.commands.get import GetExplorePermalinkCommand
 from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
 from superset.extensions import async_query_manager, cache_manager
+from superset.jinja_context import get_template_processor
 from superset.models.core import Database
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
 from superset.models.sql_lab import Query, TabState
 from superset.models.user_attributes import UserAttribute
+from superset.sql_parse import ParsedQuery
 from superset.superset_typing import FlaskResponse
 from superset.tasks.async_queries import load_explore_json_into_cache
 from superset.utils import core as utils
@@ -88,7 +98,9 @@ from superset.views.base import (
     get_error_msg,
     handle_api_exception,
     json_error_response,
+    json_errors_response,
     json_success,
+    validate_sqlatable,
 )
 from superset.views.utils import (
     bootstrap_user_data,
@@ -948,6 +960,177 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
     def log(self) -> FlaskResponse:  # pylint: disable=no-self-use
         return Response(status=200)
 
+    @has_access
+    @expose("/get_or_create_table/", methods=["POST"])
+    @event_logger.log_this
+    @deprecated(new_target="api/v1/dataset/get_or_create/")
+    def sqllab_table_viz(self) -> FlaskResponse:  # pylint: disable=no-self-use
+        """Gets or creates a table object with attributes passed to the API.
+
+        It expects the json with params:
+        * datasourceName - e.g. table name, required
+        * dbId - database id, required
+        * schema - table schema, optional
+        * templateParams - params for the Jinja templating syntax, optional
+        :return: Response
+        """
+        data = json.loads(request.form["data"])
+        table_name = data["datasourceName"]
+        database_id = data["dbId"]
+        table = (
+            db.session.query(SqlaTable)
+            .filter_by(database_id=database_id, table_name=table_name)
+            .one_or_none()
+        )
+        if not table:
+            # Create table if doesn't exist.
+            with db.session.no_autoflush:
+                table = SqlaTable(table_name=table_name, owners=[g.user])
+                table.database_id = database_id
+                table.database = (
+                    db.session.query(Database).filter_by(id=database_id).one()
+                )
+                table.schema = data.get("schema")
+                table.template_params = data.get("templateParams")
+                # needed for the table validation.
+                # fn can be deleted when this endpoint is removed
+                validate_sqlatable(table)
+
+            db.session.add(table)
+            table.fetch_metadata()
+            db.session.commit()
+
+        return json_success(json.dumps({"table_id": table.id}))
+
+    @has_access
+    @expose("/sqllab_viz/", methods=["POST"])
+    @event_logger.log_this
+    def sqllab_viz(self) -> FlaskResponse:  # pylint: disable=no-self-use
+        data = json.loads(request.form["data"])
+        try:
+            table_name = data["datasourceName"]
+            database_id = data["dbId"]
+        except KeyError as ex:
+            raise SupersetGenericErrorException(
+                __(
+                    "One or more required fields are missing in the request. Please try "
+                    "again, and if the problem persists contact your administrator."
+                ),
+                status=400,
+            ) from ex
+        database = db.session.query(Database).get(database_id)
+        if not database:
+            raise SupersetErrorException(
+                SupersetError(
+                    message=__("The database was not found."),
+                    error_type=SupersetErrorType.DATABASE_NOT_FOUND_ERROR,
+                    level=ErrorLevel.ERROR,
+                ),
+                status=404,
+            )
+        table = (
+            db.session.query(SqlaTable)
+            .filter_by(database_id=database_id, table_name=table_name)
+            .one_or_none()
+        )
+
+        if table:
+            return json_errors_response(
+                [
+                    SupersetError(
+                        message=f"Dataset [{table_name}] already exists",
+                        error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
+                        level=ErrorLevel.WARNING,
+                    )
+                ],
+                status=422,
+            )
+
+        table = SqlaTable(table_name=table_name, owners=[g.user])
+        table.database = database
+        table.schema = data.get("schema")
+        table.template_params = data.get("templateParams")
+        table.is_sqllab_view = True
+        table.sql = ParsedQuery(data.get("sql")).stripped()
+        db.session.add(table)
+        cols = []
+        for config_ in data.get("columns"):
+            column_name = config_.get("column_name") or config_.get("name")
+            col = TableColumn(
+                column_name=column_name,
+                filterable=True,
+                groupby=True,
+                is_dttm=config_.get("is_dttm", False),
+                type=config_.get("type", False),
+            )
+            cols.append(col)
+
+        table.columns = cols
+        table.metrics = [SqlMetric(metric_name="count", expression="count(*)")]
+        db.session.commit()
+
+        return json_success(
+            json.dumps(
+                {"table_id": table.id, "data": sanitize_datasource_data(table.data)}
+            )
+        )
+
+    @has_access
+    @expose("/extra_table_metadata/<int:database_id>/<table_name>/<schema>/")
+    @event_logger.log_this
+    @deprecated(
+        new_target="api/v1/database/<int:pk>/table_extra/<table_name>/<schema_name>/"
+    )
+    def extra_table_metadata(  # pylint: disable=no-self-use
+        self, database_id: int, table_name: str, schema: str
+    ) -> FlaskResponse:
+        parsed_schema = utils.parse_js_uri_path_item(schema, eval_undefined=True)
+        table_name = utils.parse_js_uri_path_item(table_name)  # type: ignore
+        mydb = db.session.query(Database).filter_by(id=database_id).one()
+        payload = mydb.db_engine_spec.extra_table_metadata(
+            mydb, table_name, parsed_schema
+        )
+        return json_success(json.dumps(payload))
+
+    @has_access_api
+    @expose("/estimate_query_cost/<int:database_id>/", methods=["POST"])
+    @expose("/estimate_query_cost/<int:database_id>/<schema>/", methods=["POST"])
+    @event_logger.log_this
+    @deprecated()
+    def estimate_query_cost(  # pylint: disable=no-self-use
+        self, database_id: int, schema: str | None = None
+    ) -> FlaskResponse:
+        mydb = db.session.query(Database).get(database_id)
+
+        sql = json.loads(request.form.get("sql", '""'))
+        if template_params := json.loads(request.form.get("templateParams") or "{}"):
+            template_processor = get_template_processor(mydb)
+            sql = template_processor.process_template(sql, **template_params)
+
+        timeout = SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT
+        timeout_msg = f"The estimation exceeded the {timeout} seconds timeout."
+        try:
+            with utils.timeout(seconds=timeout, error_message=timeout_msg):
+                cost = mydb.db_engine_spec.estimate_query_cost(
+                    mydb, schema, sql, utils.QuerySource.SQL_LAB
+                )
+        except SupersetTimeoutException as ex:
+            logger.exception(ex)
+            return json_errors_response([ex.error])
+        except Exception as ex:  # pylint: disable=broad-except
+            return json_error_response(utils.error_msg_from_exception(ex))
+
+        spec = mydb.db_engine_spec
+        query_cost_formatters: dict[str, Any] = app.config[
+            "QUERY_COST_FORMATTERS_BY_ENGINE"
+        ]
+        query_cost_formatter = query_cost_formatters.get(
+            spec.engine, spec.query_cost_formatter
+        )
+        cost = query_cost_formatter(cost)
+
+        return json_success(json.dumps(cost))
+
     @expose("/theme/")
     def theme(self) -> FlaskResponse:
         return self.render_template("superset/theme.html")