You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2021/02/03 02:01:51 UTC

[superset] branch master updated: feat: add separate endpoint to fetch function names for autocomplete (#12840)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new ab3f4bd  feat: add separate endpoint to fetch function names for autocomplete (#12840)
ab3f4bd is described below

commit ab3f4bd94b7dc75d7a64b07e494e83d730f614c7
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Tue Feb 2 18:01:01 2021 -0800

    feat: add separate endpoint to fetch function names for autocomplete (#12840)
    
    * WIP
    
    * Add unit test for API
    
    * Add spec
    
    * Fix unit test
    
    * Fix unit test
    
    * Fix test
    
    * Fix test
    
    * Add period to error message
---
 .../spec/javascripts/sqllab/SqlEditor_spec.jsx     |  7 +++-
 superset-frontend/src/SqlLab/actions/sqlLab.js     | 22 ++++++++++
 .../src/SqlLab/components/AceEditorWrapper.tsx     |  5 +++
 .../src/SqlLab/components/SqlEditor.jsx            |  4 +-
 .../src/SqlLab/components/SqlEditorLeftBar.jsx     |  4 ++
 .../src/SqlLab/reducers/getInitialState.js         |  2 +
 superset-frontend/src/SqlLab/reducers/sqlLab.js    |  5 +++
 superset/databases/api.py                          | 48 ++++++++++++++++++++--
 superset/databases/schemas.py                      |  6 ++-
 tests/databases/api_tests.py                       | 21 +++++++++-
 10 files changed, 113 insertions(+), 11 deletions(-)

diff --git a/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx
index eca063c..2ba6268 100644
--- a/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx
+++ b/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx
@@ -33,7 +33,10 @@ import ConnectedSouthPane from 'src/SqlLab/components/SouthPane';
 import SqlEditor from 'src/SqlLab/components/SqlEditor';
 import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar';
 import { Dropdown } from 'src/common/components';
-import { queryEditorSetSelectedText } from 'src/SqlLab/actions/sqlLab';
+import {
+  queryEditorSetFunctionNames,
+  queryEditorSetSelectedText,
+} from 'src/SqlLab/actions/sqlLab';
 
 import { initialState, queries, table } from './fixtures';
 
@@ -45,7 +48,7 @@ const store = mockStore(initialState);
 
 describe('SqlEditor', () => {
   const mockedProps = {
-    actions: { queryEditorSetSelectedText },
+    actions: { queryEditorSetFunctionNames, queryEditorSetSelectedText },
     database: {},
     queryEditorId: initialState.sqlLab.queryEditors[0].id,
     latestQuery: queries[0],
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 647cfad..090d69c 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -57,6 +57,8 @@ export const QUERY_EDITOR_SET_QUERY_LIMIT = 'QUERY_EDITOR_SET_QUERY_LIMIT';
 export const QUERY_EDITOR_SET_TEMPLATE_PARAMS =
   'QUERY_EDITOR_SET_TEMPLATE_PARAMS';
 export const QUERY_EDITOR_SET_SELECTED_TEXT = 'QUERY_EDITOR_SET_SELECTED_TEXT';
+export const QUERY_EDITOR_SET_FUNCTION_NAMES =
+  'QUERY_EDITOR_SET_FUNCTION_NAMES';
 export const QUERY_EDITOR_PERSIST_HEIGHT = 'QUERY_EDITOR_PERSIST_HEIGHT';
 export const MIGRATE_QUERY_EDITOR = 'MIGRATE_QUERY_EDITOR';
 export const MIGRATE_TAB_HISTORY = 'MIGRATE_TAB_HISTORY';
@@ -1300,3 +1302,23 @@ export function createCtasDatasource(vizOptions) {
       });
   };
 }
+
+export function queryEditorSetFunctionNames(queryEditor, dbId) {
+  return function (dispatch) {
+    return SupersetClient.get({
+      endpoint: encodeURI(`/api/v1/database/${dbId}/function_names/`),
+    })
+      .then(({ json }) =>
+        dispatch({
+          type: QUERY_EDITOR_SET_FUNCTION_NAMES,
+          queryEditor,
+          functionNames: json.function_names,
+        }),
+      )
+      .catch(() =>
+        dispatch(
+          addDangerToast(t('An error occurred while fetching function names.')),
+        ),
+      );
+  };
+}
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper.tsx
index 95c6eb8..3c33f4c 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper.tsx
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper.tsx
@@ -41,6 +41,7 @@ type HotKey = {
 interface Props {
   actions: {
     queryEditorSetSelectedText: (edit: any, text: null | string) => void;
+    queryEditorSetFunctionNames: (queryEditor: object, dbId: number) => void;
     addTable: (queryEditor: any, value: any, schema: any) => void;
   };
   autocomplete: boolean;
@@ -85,6 +86,10 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
   componentDidMount() {
     // Making sure no text is selected from previous mount
     this.props.actions.queryEditorSetSelectedText(this.props.queryEditor, null);
+    this.props.actions.queryEditorSetFunctionNames(
+      this.props.queryEditor,
+      this.props.queryEditor.dbId,
+    );
     this.setAutoCompleter(this.props);
   }
 
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor.jsx b/superset-frontend/src/SqlLab/components/SqlEditor.jsx
index ee89a0f..3af2b1f 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor.jsx
@@ -474,9 +474,7 @@ class SqlEditor extends React.PureComponent {
             sql={this.props.queryEditor.sql}
             schemas={this.props.queryEditor.schemaOptions}
             tables={this.props.queryEditor.tableOptions}
-            functionNames={
-              this.props.database ? this.props.database.function_names : []
-            }
+            functionNames={this.props.queryEditor.functionNames}
             extendedTables={this.props.tables}
             height={`${aceEditorHeight}px`}
             hotkeys={hotkeys}
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
index 550f623..7136ab0 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx
@@ -80,6 +80,10 @@ export default class SqlEditorLeftBar extends React.PureComponent {
 
   onDbChange(db) {
     this.props.actions.queryEditorSetDb(this.props.queryEditor, db.id);
+    this.props.actions.queryEditorSetFunctionNames(
+      this.props.queryEditor,
+      db.id,
+    );
   }
 
   onTableChange(tableName, schemaName) {
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.js b/superset-frontend/src/SqlLab/reducers/getInitialState.js
index 60cd420..df55748 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.js
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.js
@@ -48,6 +48,7 @@ export default function getInitialState({
     autorun: false,
     templateParams: null,
     dbId: defaultDbId,
+    functionNames: [],
     queryLimit: common.conf.DEFAULT_SQLLAB_LIMIT,
     validationResult: {
       id: null,
@@ -80,6 +81,7 @@ export default function getInitialState({
         autorun: activeTab.autorun,
         templateParams: activeTab.template_params,
         dbId: activeTab.database_id,
+        functionNames: [],
         schema: activeTab.schema,
         queryLimit: activeTab.query_limit,
         validationResult: {
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index 8a94bcb..a5930d2 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -434,6 +434,11 @@ export default function sqlLabReducer(state = {}, action) {
         dbId: action.dbId,
       });
     },
+    [actions.QUERY_EDITOR_SET_FUNCTION_NAMES]() {
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        functionNames: action.functionNames,
+      });
+    },
     [actions.QUERY_EDITOR_SET_SCHEMA]() {
       return alterInArr(state, 'queryEditors', action.queryEditor, {
         schema: action.schema,
diff --git a/superset/databases/api.py b/superset/databases/api.py
index e8273eb..ba665c1 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -53,6 +53,7 @@ from superset.databases.decorators import check_datasource_access
 from superset.databases.filters import DatabaseFilter
 from superset.databases.schemas import (
     database_schemas_query_schema,
+    DatabaseFunctionNamesResponse,
     DatabasePostSchema,
     DatabasePutSchema,
     DatabaseRelatedObjectsResponse,
@@ -83,6 +84,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         "schemas",
         "test_connection",
         "related_objects",
+        "function_names",
     }
     resource_name = "database"
     class_permission_name = "Database"
@@ -126,7 +128,6 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         "explore_database_id",
         "expose_in_sqllab",
         "force_ctas_schema",
-        "function_names",
         "id",
     ]
     add_columns = [
@@ -170,6 +171,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
     }
     openapi_spec_tag = "Database"
     openapi_spec_component_schemas = (
+        DatabaseFunctionNamesResponse,
         DatabaseRelatedObjectsResponse,
         DatabaseTestConnectionSchema,
         TableMetadataResponseSchema,
@@ -642,8 +644,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
             500:
               $ref: '#/components/responses/500'
         """
-        dataset = DatabaseDAO.find_by_id(pk)
-        if not dataset:
+        database = DatabaseDAO.find_by_id(pk)
+        if not database:
             return self.response_404()
         data = DatabaseDAO.get_related_objects(pk)
         charts = [
@@ -799,3 +801,43 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
         except DatabaseImportError as exc:
             logger.exception("Import database failed")
             return self.response_500(message=str(exc))
+
+    @expose("/<int:pk>/function_names/", methods=["GET"])
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
+        f".function_names",
+        log_to_statsd=False,
+    )
+    def function_names(self, pk: int) -> Response:
+        """Get function names supported by a database
+        ---
+        get:
+          description:
+            Get function names supported by a database
+          parameters:
+          - in: path
+            name: pk
+            schema:
+              type: integer
+          responses:
+            200:
+            200:
+              description: Query result
+              content:
+                application/json:
+                  schema:
+                    $ref: "#/components/schemas/DatabaseFunctionNamesResponse"
+            401:
+              $ref: '#/components/responses/401'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        database = DatabaseDAO.find_by_id(pk)
+        if not database:
+            return self.response_404()
+        return self.response(200, function_names=database.function_names,)
diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py
index 2c705f3..0fbcf1d 100644
--- a/superset/databases/schemas.py
+++ b/superset/databases/schemas.py
@@ -413,11 +413,15 @@ class DatabaseRelatedObjectsResponse(Schema):
     dashboards = fields.Nested(DatabaseRelatedDashboards)
 
 
+class DatabaseFunctionNamesResponse(Schema):
+    function_names = fields.List(fields.String())
+
+
 class ImportV1DatabaseExtraSchema(Schema):
     metadata_params = fields.Dict(keys=fields.Str(), values=fields.Raw())
     engine_params = fields.Dict(keys=fields.Str(), values=fields.Raw())
     metadata_cache_timeout = fields.Dict(keys=fields.Str(), values=fields.Integer())
-    schemas_allowed_for_csv_upload = fields.List(fields.String)
+    schemas_allowed_for_csv_upload = fields.List(fields.String())
     cost_estimate_enabled = fields.Boolean()
 
 
diff --git a/tests/databases/api_tests.py b/tests/databases/api_tests.py
index 8d603cc..a72c7eb 100644
--- a/tests/databases/api_tests.py
+++ b/tests/databases/api_tests.py
@@ -137,7 +137,6 @@ class TestDatabaseApi(SupersetTestCase):
             "explore_database_id",
             "expose_in_sqllab",
             "force_ctas_schema",
-            "function_names",
             "id",
         ]
         self.assertGreater(response["count"], 0)
@@ -589,7 +588,8 @@ class TestDatabaseApi(SupersetTestCase):
         assert rv.status_code == 200
         assert "can_read" in data["permissions"]
         assert "can_write" in data["permissions"]
-        assert len(data["permissions"]) == 2
+        assert "can_function_names" in data["permissions"]
+        assert len(data["permissions"]) == 3
 
     def test_get_invalid_database_table_metadata(self):
         """
@@ -1125,3 +1125,20 @@ class TestDatabaseApi(SupersetTestCase):
 
         db.session.delete(database)
         db.session.commit()
+
+    @mock.patch("superset.db_engine_specs.base.BaseEngineSpec.get_function_names",)
+    def test_function_names(self, mock_get_function_names):
+        example_db = get_example_database()
+        if example_db.backend in {"hive", "presto"}:
+            return
+
+        mock_get_function_names.return_value = ["AVG", "MAX", "SUM"]
+
+        self.login(username="admin")
+        uri = "api/v1/database/1/function_names/"
+
+        rv = self.client.get(uri)
+        response = json.loads(rv.data.decode("utf-8"))
+
+        assert rv.status_code == 200
+        assert response == {"function_names": ["AVG", "MAX", "SUM"]}