Skip to content

Commit

Permalink
chore: migrate /sql_json and /results to apiv1 (#22809)
Browse files Browse the repository at this point in the history
  • Loading branch information
diegomedina248 authored Jan 30, 2023
1 parent c9b7507 commit b94052e
Show file tree
Hide file tree
Showing 38 changed files with 1,449 additions and 869 deletions.
1,428 changes: 595 additions & 833 deletions docs/static/resources/openapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('SqlLab query panel', () => {

cy.intercept({
method: 'POST',
url: '/superset/sql_json/',
url: '/api/v1/sqllab/execute/',
}).as('mockSQLResponse');

cy.get('.TableSelector .Select:eq(0)').click();
Expand Down Expand Up @@ -148,7 +148,7 @@ describe('SqlLab query panel', () => {
});

it('Create a chart from a query', () => {
cy.intercept('/superset/sql_json/').as('queryFinished');
cy.intercept('/api/v1/sqllab/execute/').as('queryFinished');
cy.intercept('**/api/v1/explore/**').as('explore');
cy.intercept('**/api/v1/chart/**').as('chart');

Expand Down
16 changes: 13 additions & 3 deletions superset-frontend/src/SqlLab/actions/sqlLab.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/
import shortid from 'shortid';
import rison from 'rison';
import { SupersetClient, t } from '@superset-ui/core';
import invert from 'lodash/invert';
import mapKeys from 'lodash/mapKeys';
Expand Down Expand Up @@ -305,8 +306,13 @@ export function fetchQueryResults(query, displayLimit) {
return function (dispatch) {
dispatch(requestQueryResults(query));

const queryParams = rison.encode({
key: query.resultsKey,
rows: displayLimit || null,
});

return SupersetClient.get({
endpoint: `/superset/results/${query.resultsKey}/?rows=${displayLimit}`,
endpoint: `/api/v1/sqllab/results/?q=${queryParams}`,
parseMethod: 'json-bigint',
})
.then(({ json }) => dispatch(querySuccess(query, json)))
Expand Down Expand Up @@ -347,7 +353,7 @@ export function runQuery(query) {

const search = window.location.search || '';
return SupersetClient.post({
endpoint: `/superset/sql_json/${search}`,
endpoint: `/api/v1/sqllab/execute/${search}`,
body: JSON.stringify(postPayload),
headers: { 'Content-Type': 'application/json' },
parseMethod: 'json-bigint',
Expand All @@ -359,7 +365,11 @@ export function runQuery(query) {
})
.catch(response =>
getClientErrorObject(response).then(error => {
let message = error.error || error.statusText || t('Unknown error');
let message =
error.error ||
error.message ||
error.statusText ||
t('Unknown error');
if (message.includes('CSRF token')) {
message = t(COMMON_ERR_MESSAGES.SESSION_TIMED_OUT);
}
Expand Down
7 changes: 4 additions & 3 deletions superset-frontend/src/SqlLab/actions/sqlLab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ describe('async actions', () => {

afterEach(fetchMock.resetHistory);

const fetchQueryEndpoint = 'glob:*/superset/results/*';
const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*';
fetchMock.get(
fetchQueryEndpoint,
JSON.stringify({ data: mockBigNumber, query: { sqlEditorId: 'dfsadfs' } }),
);

const runQueryEndpoint = 'glob:*/superset/sql_json/';
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);

describe('saveQuery', () => {
Expand Down Expand Up @@ -280,7 +280,8 @@ describe('async actions', () => {
};

it('makes the fetch request', async () => {
const runQueryEndpointWithParams = 'glob:*/superset/sql_json/?foo=bar';
const runQueryEndpointWithParams =
'glob:*/api/v1/sqllab/execute/?foo=bar';
fetchMock.post(
runQueryEndpointWithParams,
`{ "data": ${mockBigNumber} }`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const MOCKED_SQL_EDITOR_HEIGHT = 500;

fetchMock.get('glob:*/api/v1/database/*', { result: [] });
fetchMock.get('glob:*/superset/tables/*', { options: [] });
fetchMock.post('glob:*/sql_json/*', { result: [] });
fetchMock.post('glob:*/sqllab/execute/*', { result: [] });

const middlewares = [thunk];
const mockStore = configureStore(middlewares);
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/SqlLab/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,7 @@ export const query = {
sql: 'SELECT * FROM something',
description: 'test description',
schema: 'test schema',
resultsKey: 'test',
};

export const queryId = 'clientId2353';
Expand Down
2 changes: 2 additions & 0 deletions superset/initialization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def init_views(self) -> None:
from superset.reports.api import ReportScheduleRestApi
from superset.reports.logs.api import ReportExecutionLogRestApi
from superset.security.api import SecurityRestApi
from superset.sqllab.api import SqlLabRestApi
from superset.views.access_requests import AccessRequestsModelView
from superset.views.alerts import AlertView, ReportView
from superset.views.annotations import AnnotationLayerView
Expand Down Expand Up @@ -219,6 +220,7 @@ def init_views(self) -> None:
appbuilder.add_api(ReportScheduleRestApi)
appbuilder.add_api(ReportExecutionLogRestApi)
appbuilder.add_api(SavedQueryRestApi)
appbuilder.add_api(SqlLabRestApi)
#
# Setup regular views
#
Expand Down
248 changes: 248 additions & 0 deletions superset/sqllab/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# 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 logging
from typing import Any, cast, Dict, Optional

import simplejson as json
from flask import request
from flask_appbuilder.api import expose, protect, rison
from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import ValidationError

from superset import app, is_feature_enabled
from superset.databases.dao import DatabaseDAO
from superset.extensions import event_logger
from superset.jinja_context import get_template_processor
from superset.models.sql_lab import Query
from superset.queries.dao import QueryDAO
from superset.sql_lab import get_sql_results
from superset.sqllab.command_status import SqlJsonExecutionStatus
from superset.sqllab.commands.execute import CommandResult, ExecuteSqlCommand
from superset.sqllab.commands.results import SqlExecutionResultsCommand
from superset.sqllab.exceptions import (
QueryIsForbiddenToAccessException,
SqlLabException,
)
from superset.sqllab.execution_context_convertor import ExecutionContextConvertor
from superset.sqllab.query_render import SqlQueryRenderImpl
from superset.sqllab.schemas import (
ExecutePayloadSchema,
QueryExecutionResponseSchema,
sql_lab_get_results_schema,
)
from superset.sqllab.sql_json_executer import (
ASynchronousSqlJsonExecutor,
SqlJsonExecutor,
SynchronousSqlJsonExecutor,
)
from superset.sqllab.sqllab_execution_context import SqlJsonExecutionContext
from superset.sqllab.validators import CanAccessQueryValidatorImpl
from superset.superset_typing import FlaskResponse
from superset.utils import core as utils
from superset.views.base import json_success
from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics

config = app.config
logger = logging.getLogger(__name__)


class SqlLabRestApi(BaseSupersetApi):
datamodel = SQLAInterface(Query)

resource_name = "sqllab"
allow_browser_login = True

class_permission_name = "Query"

execute_model_schema = ExecutePayloadSchema()

apispec_parameter_schemas = {
"sql_lab_get_results_schema": sql_lab_get_results_schema,
}
openapi_spec_tag = "SQL Lab"
openapi_spec_component_schemas = (
ExecutePayloadSchema,
QueryExecutionResponseSchema,
)

@expose("/results/")
@protect()
@statsd_metrics
@rison(sql_lab_get_results_schema)
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
f".get_results",
log_to_statsd=False,
)
def get_results(self, **kwargs: Any) -> FlaskResponse:
"""Gets the result of a SQL query execution
---
get:
summary: >-
Gets the result of a SQL query execution
parameters:
- in: query
name: q
content:
application/json:
schema:
$ref: '#/components/schemas/sql_lab_get_results_schema'
responses:
200:
description: SQL query execution result
content:
application/json:
schema:
$ref: '#/components/schemas/QueryExecutionResponseSchema'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
404:
$ref: '#/components/responses/404'
410:
$ref: '#/components/responses/410'
500:
$ref: '#/components/responses/500'
"""
params = kwargs["rison"]
key = params.get("key")
rows = params.get("rows")
result = SqlExecutionResultsCommand(key=key, rows=rows).run()
# return the result without special encoding
return json_success(
json.dumps(
result, default=utils.json_iso_dttm_ser, ignore_nan=True, encoding=None
),
200,
)

@expose("/execute/", methods=["POST"])
@protect()
@statsd_metrics
@requires_json
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
f".get_results",
log_to_statsd=False,
)
def execute_sql_query(self) -> FlaskResponse:
"""Executes a SQL query
---
post:
description: >-
Starts the execution of a SQL query
requestBody:
description: SQL query and params
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExecutePayloadSchema'
responses:
200:
description: Query execution result
content:
application/json:
schema:
$ref: '#/components/schemas/QueryExecutionResponseSchema'
202:
description: Query execution result, query still running
content:
application/json:
schema:
$ref: '#/components/schemas/QueryExecutionResponseSchema'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
404:
$ref: '#/components/responses/404'
500:
$ref: '#/components/responses/500'
"""
try:
self.execute_model_schema.load(request.json)
except ValidationError as error:
return self.response_400(message=error.messages)

try:
log_params = {
"user_agent": cast(Optional[str], request.headers.get("USER_AGENT"))
}
execution_context = SqlJsonExecutionContext(request.json)
command = self._create_sql_json_command(execution_context, log_params)
command_result: CommandResult = command.run()

response_status = (
202
if command_result["status"] == SqlJsonExecutionStatus.QUERY_IS_RUNNING
else 200
)
# return the execution result without special encoding
return json_success(command_result["payload"], response_status)
except SqlLabException as ex:
payload = {"errors": [ex.to_dict()]}

response_status = (
403 if isinstance(ex, QueryIsForbiddenToAccessException) else ex.status
)
return self.response(response_status, **payload)

@staticmethod
def _create_sql_json_command(
execution_context: SqlJsonExecutionContext, log_params: Optional[Dict[str, Any]]
) -> ExecuteSqlCommand:
query_dao = QueryDAO()
sql_json_executor = SqlLabRestApi._create_sql_json_executor(
execution_context, query_dao
)
execution_context_convertor = ExecutionContextConvertor()
execution_context_convertor.set_max_row_in_display(
int(config.get("DISPLAY_MAX_ROW")) # type: ignore
)
return ExecuteSqlCommand(
execution_context,
query_dao,
DatabaseDAO(),
CanAccessQueryValidatorImpl(),
SqlQueryRenderImpl(get_template_processor),
sql_json_executor,
execution_context_convertor,
config.get("SQLLAB_CTAS_NO_LIMIT"),
log_params,
)

@staticmethod
def _create_sql_json_executor(
execution_context: SqlJsonExecutionContext, query_dao: QueryDAO
) -> SqlJsonExecutor:
sql_json_executor: SqlJsonExecutor
if execution_context.is_run_asynchronous():
sql_json_executor = ASynchronousSqlJsonExecutor(query_dao, get_sql_results)
else:
sql_json_executor = SynchronousSqlJsonExecutor(
query_dao,
get_sql_results,
config.get("SQLLAB_TIMEOUT"), # type: ignore
is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE"),
)
return sql_json_executor
File renamed without changes.
Loading

0 comments on commit b94052e

Please sign in to comment.