feat(sqllab): Add /sqllab endpoint to the v1 api (#24983)

This commit is contained in:
JUST.in DO IT
2023-08-16 16:09:10 -07:00
committed by GitHub
parent 2eb0a255d9
commit 10abb68288
5 changed files with 185 additions and 49 deletions

View File

@@ -236,6 +236,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
("can_execute_sql_query", "SQLLab"),
("can_estimate_query_cost", "SQL Lab"),
("can_export_csv", "SQLLab"),
("can_read", "SQLLab"),
("can_sqllab_history", "Superset"),
("can_sqllab", "Superset"),
("can_test_conn", "Superset"), # Deprecated permission remove on 3.0.0

View File

@@ -20,7 +20,7 @@ from urllib import parse
import simplejson as json
from flask import request, Response
from flask_appbuilder.api import expose, protect, rison
from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import ValidationError
@@ -47,6 +47,7 @@ from superset.sqllab.schemas import (
ExecutePayloadSchema,
QueryExecutionResponseSchema,
sql_lab_get_results_schema,
SQLLabBootstrapSchema,
)
from superset.sqllab.sql_json_executer import (
ASynchronousSqlJsonExecutor,
@@ -54,6 +55,7 @@ from superset.sqllab.sql_json_executer import (
SynchronousSqlJsonExecutor,
)
from superset.sqllab.sqllab_execution_context import SqlJsonExecutionContext
from superset.sqllab.utils import bootstrap_sqllab_data
from superset.sqllab.validators import CanAccessQueryValidatorImpl
from superset.superset_typing import FlaskResponse
from superset.utils import core as utils
@@ -83,8 +85,55 @@ class SqlLabRestApi(BaseSupersetApi):
EstimateQueryCostSchema,
ExecutePayloadSchema,
QueryExecutionResponseSchema,
SQLLabBootstrapSchema,
)
@expose("/", methods=("GET",))
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
log_to_statsd=False,
)
def get(self) -> Response:
"""Get the bootstrap data for SqlLab
---
get:
summary: Get the bootstrap data for SqlLab page
description: >-
Assembles SQLLab bootstrap data (active_tab, databases, queries,
tab_state_ids) in a single endpoint. The data can be assembled
from the current user's id.
responses:
200:
description: Returns the initial bootstrap data for SqlLab
content:
application/json:
schema:
$ref: '#/components/schemas/SQLLabBootstrapSchema'
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
500:
$ref: '#/components/responses/500'
"""
user_id = utils.get_user_id()
# TODO: Replace with a command class once fully migrated to SPA
result = bootstrap_sqllab_data(user_id)
return json_success(
json.dumps(
{"result": result},
default=utils.json_iso_dttm_ser,
ignore_nan=True,
),
200,
)
@expose("/estimate/", methods=("POST",))
@protect()
@statsd_metrics

View File

@@ -16,6 +16,8 @@
# under the License.
from marshmallow import fields, Schema
from superset.databases.schemas import ImportV1DatabaseSchema
sql_lab_get_results_schema = {
"type": "object",
"properties": {
@@ -95,3 +97,50 @@ class QueryExecutionResponseSchema(Schema):
expanded_columns = fields.List(fields.Dict())
query = fields.Nested(QueryResultSchema)
query_id = fields.Integer()
class TableSchema(Schema):
database_id = fields.Integer()
description = fields.String()
expanded = fields.Boolean()
id = fields.Integer()
schema = fields.String()
tab_state_id = fields.Integer()
table = fields.String()
class TabStateSchema(Schema):
active = fields.Boolean()
autorun = fields.Boolean()
database_id = fields.Integer()
extra_json = fields.Dict()
hide_left_bar = fields.Boolean()
id = fields.String()
label = fields.String()
latest_query = fields.Nested(QueryResultSchema)
query_limit = fields.Integer()
saved_query = fields.Dict(
allow_none=True,
metadata={"id": "SavedQuery id"},
)
schema = fields.String()
sql = fields.String()
table_schemas = fields.List(fields.Nested(TableSchema))
user_id = fields.Integer()
class SQLLabBootstrapSchema(Schema):
active_tab = fields.Nested(TabStateSchema)
databases = fields.Dict(
keys=fields.String(
metadata={"description": "Database id"},
),
values=fields.Nested(ImportV1DatabaseSchema),
)
queries = fields.Dict(
keys=fields.String(
metadata={"description": "Query id"},
),
values=fields.Nested(QueryResultSchema),
)
tab_state_ids = fields.List(fields.String())

View File

@@ -1090,54 +1090,6 @@ class TestCore(SupersetTestCase, InsertChartMixin):
data = self.get_resp(url)
self.assertTrue(html_string in data)
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
{"SQLLAB_BACKEND_PERSISTENCE": True},
clear=True,
)
def test_sqllab_backend_persistence_payload(self):
username = "admin"
self.login(username)
user_id = security_manager.find_user(username).id
# create a tab
data = {
"queryEditor": json.dumps(
{
"title": "Untitled Query 1",
"dbId": 1,
"schema": None,
"autorun": False,
"sql": "SELECT ...",
"queryLimit": 1000,
}
)
}
resp = self.get_json_resp("/tabstateview/", data=data)
tab_state_id = resp["id"]
# run a query in the created tab
self.run_sql(
"SELECT name FROM birth_names",
"client_id_1",
username=username,
raise_on_error=True,
sql_editor_id=str(tab_state_id),
)
# run an orphan query (no tab)
self.run_sql(
"SELECT name FROM birth_names",
"client_id_2",
username=username,
raise_on_error=True,
)
# we should have only 1 query returned, since the second one is not
# associated with any tabs
# TODO: replaces this spec by api/v1/sqllab spec later
payload = bootstrap_sqllab_data(user_id)
self.assertEqual(len(payload["queries"]), 1)
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
{"SQLLAB_BACKEND_PERSISTENCE": True},

View File

@@ -28,6 +28,7 @@ import prison
from sqlalchemy.sql import func
from unittest import mock
from flask_appbuilder.security.sqla.models import Role
from tests.integration_tests.test_app import app
from superset import db, sql_lab
from superset.common.db_query_status import QueryStatus
@@ -37,11 +38,95 @@ from superset.utils import core as utils
from superset.models.sql_lab import Query
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.fixtures.users import create_gamma_sqllab_no_data
QUERIES_FIXTURE_COUNT = 10
class TestSqlLabApi(SupersetTestCase):
@pytest.mark.usefixtures("create_gamma_sqllab_no_data")
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
{"SQLLAB_BACKEND_PERSISTENCE": False},
clear=True,
)
def test_get_from_empty_bootsrap_data(self):
self.login(username="gamma_sqllab_no_data")
resp = self.client.get("/api/v1/sqllab/")
assert resp.status_code == 200
data = json.loads(resp.data.decode("utf-8"))
result = data.get("result")
assert result["active_tab"] == None
assert result["queries"] == {}
assert result["tab_state_ids"] == []
self.assertEqual(len(result["databases"]), 0)
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
{"SQLLAB_BACKEND_PERSISTENCE": True},
clear=True,
)
def test_get_from_bootstrap_data_with_queries(self):
username = "admin"
self.login(username)
# create a tab
data = {
"queryEditor": json.dumps(
{
"title": "Untitled Query 1",
"dbId": 1,
"schema": None,
"autorun": False,
"sql": "SELECT ...",
"queryLimit": 1000,
}
)
}
resp = self.get_json_resp("/tabstateview/", data=data)
tab_state_id = resp["id"]
# run a query in the created tab
self.run_sql(
"SELECT name FROM birth_names",
"client_id_1",
username=username,
raise_on_error=True,
sql_editor_id=str(tab_state_id),
)
# run an orphan query (no tab)
self.run_sql(
"SELECT name FROM birth_names",
"client_id_2",
username=username,
raise_on_error=True,
)
# we should have only 1 query returned, since the second one is not
# associated with any tabs
resp = self.get_json_resp("/api/v1/sqllab/")
result = resp["result"]
self.assertEqual(len(result["queries"]), 1)
def test_get_access_denied(self):
new_role = Role(name="Dummy Role", permissions=[])
db.session.add(new_role)
db.session.commit()
unauth_user = self.create_user(
"unauth_user1",
"password",
"Dummy Role",
email=f"unauth_user1@superset.org",
)
self.login(username="unauth_user1", password="password")
rv = self.client.get("/api/v1/sqllab/")
assert rv.status_code == 403
db.session.delete(unauth_user)
db.session.delete(new_role)
db.session.commit()
def test_estimate_required_params(self):
self.login()