mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
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
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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.')),
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
Reference in New Issue
Block a user