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:19 UTC

[superset] branch 2023.25.1 created (now 249c05262e)

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

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


      at 249c05262e fix merge conflicts

This branch includes the following new commits:

     new 249c05262e fix merge conflicts

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[superset] 01/01: fix merge conflicts

Posted by hu...@apache.org.
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")