SIP-23: Persist SQL Lab state in the backend (#8060)

* Squash all commits from VIZ-689

* Fix javascript

* Fix black

* WIP fixing javascript

* Add feature flag SQLLAB_BACKEND_PERSISTENCE

* Use feature flag

* Small fix

* Fix lint

* Fix setQueryEditorSql

* Improve unit tests

* Add unit tests for backend sync

* Rename results to description in table_schema

* Add integration tests

* Fix black

* Migrate query history

* Handle no results backend

* Small improvement

* Address comments

* Store SQL directly instead of reference to query

* Small fixes

* Fix clone tab

* Fix remove query

* Cascade delete

* Cascade deletes

* Fix tab closing

* Small fixes

* Small fix

* Fix error when deleting tab

* Catch 404 when tab is deleted

* Remove tables from state on tab close

* Add index, autoincrement and cascade

* Prevent duplicate table schemas

* Fix mapStateToProps

* Fix lint

* Fix head

* Fix javascript

* Fix mypy

* Fix isort

* Fix javascript

* Fix merge

* Fix heads

* Fix heads

* Fix displayLimit

* Recreate migration script trying to fix heads

* Fix heads
This commit is contained in:
Beto Dealmeida
2019-11-14 09:44:57 -08:00
committed by GitHub
parent 59bc220602
commit d66bc5ad90
26 changed files with 2814 additions and 347 deletions

View File

@@ -18,18 +18,24 @@
from typing import Callable
import simplejson as json
from flask import g, redirect
from flask import g, redirect, request, Response
from flask_appbuilder import expose
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access, has_access_api
from flask_babel import gettext as __, lazy_gettext as _
from flask_sqlalchemy import BaseQuery
from superset import appbuilder, get_feature_flags, security_manager
from superset.models.sql_lab import Query, SavedQuery
from superset import appbuilder, db, get_feature_flags, security_manager
from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState
from superset.utils import core as utils
from .base import BaseSupersetView, DeleteMixin, SupersetFilter, SupersetModelView
from .base import (
BaseSupersetView,
DeleteMixin,
json_success,
SupersetFilter,
SupersetModelView,
)
class QueryFilter(SupersetFilter):
@@ -169,6 +175,165 @@ class SavedQueryViewApi(SavedQueryView):
appbuilder.add_view_no_menu(SavedQueryViewApi)
appbuilder.add_view_no_menu(SavedQueryView)
class TabStateView(BaseSupersetView):
def _get_owner_id(self, tab_state_id):
return db.session.query(TabState.user_id).filter_by(id=tab_state_id).scalar()
@has_access_api
@expose("/", methods=["POST"])
def post(self):
query_editor = json.loads(request.form["queryEditor"])
tab_state = TabState(
user_id=g.user.get_id(),
label=query_editor.get("title", "Untitled Query"),
active=True,
database_id=query_editor["dbId"],
schema=query_editor.get("schema"),
sql=query_editor.get("sql", "SELECT ..."),
query_limit=query_editor.get("queryLimit"),
)
(
db.session.query(TabState)
.filter_by(user_id=g.user.get_id())
.update({"active": False})
)
db.session.add(tab_state)
db.session.commit()
return json_success(json.dumps({"id": tab_state.id}))
@has_access_api
@expose("/<int:tab_state_id>", methods=["DELETE"])
def delete(self, tab_state_id):
if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
return Response(status=403)
db.session.query(TabState).filter(TabState.id == tab_state_id).delete(
synchronize_session=False
)
db.session.query(TableSchema).filter(
TableSchema.tab_state_id == tab_state_id
).delete(synchronize_session=False)
db.session.commit()
return json_success(json.dumps("OK"))
@has_access_api
@expose("/<int:tab_state_id>", methods=["GET"])
def get(self, tab_state_id):
if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
return Response(status=403)
tab_state = db.session.query(TabState).filter_by(id=tab_state_id).first()
if tab_state is None:
return Response(status=404)
return json_success(
json.dumps(tab_state.to_dict(), default=utils.json_iso_dttm_ser)
)
@has_access_api
@expose("<int:tab_state_id>/activate", methods=["POST"])
def activate(self, tab_state_id):
owner_id = self._get_owner_id(tab_state_id)
if owner_id is None:
return Response(status=404)
if owner_id != int(g.user.get_id()):
return Response(status=403)
(
db.session.query(TabState)
.filter_by(user_id=g.user.get_id())
.update({"active": TabState.id == tab_state_id})
)
db.session.commit()
return json_success(json.dumps(tab_state_id))
@has_access_api
@expose("<int:tab_state_id>", methods=["PUT"])
def put(self, tab_state_id):
if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
return Response(status=403)
fields = {k: json.loads(v) for k, v in request.form.to_dict().items()}
db.session.query(TabState).filter_by(id=tab_state_id).update(fields)
db.session.commit()
return json_success(json.dumps(tab_state_id))
@has_access_api
@expose("<int:tab_state_id>/migrate_query", methods=["POST"])
def migrate_query(self, tab_state_id):
if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
return Response(status=403)
client_id = json.loads(request.form["queryId"])
db.session.query(Query).filter_by(client_id=client_id).update(
{"sql_editor_id": tab_state_id}
)
db.session.commit()
return json_success(json.dumps(tab_state_id))
@has_access_api
@expose("<int:tab_state_id>/query/<client_id>", methods=["DELETE"])
def delete_query(self, tab_state_id, client_id):
db.session.query(Query).filter_by(
client_id=client_id, user_id=g.user.get_id(), sql_editor_id=tab_state_id
).delete(synchronize_session=False)
db.session.commit()
return json_success(json.dumps("OK"))
class TableSchemaView(BaseSupersetView):
@has_access_api
@expose("/", methods=["POST"])
def post(self):
table = json.loads(request.form["table"])
# delete any existing table schema
db.session.query(TableSchema).filter(
TableSchema.tab_state_id == table["queryEditorId"],
TableSchema.database_id == table["dbId"],
TableSchema.schema == table["schema"],
TableSchema.table == table["name"],
).delete(synchronize_session=False)
table_schema = TableSchema(
tab_state_id=table["queryEditorId"],
database_id=table["dbId"],
schema=table["schema"],
table=table["name"],
description=json.dumps(table),
expanded=True,
)
db.session.add(table_schema)
db.session.commit()
return json_success(json.dumps({"id": table_schema.id}))
@has_access_api
@expose("/<int:table_schema_id>", methods=["DELETE"])
def delete(self, table_schema_id):
db.session.query(TableSchema).filter(TableSchema.id == table_schema_id).delete(
synchronize_session=False
)
db.session.commit()
return json_success(json.dumps("OK"))
@has_access_api
@expose("/<int:table_schema_id>/expanded", methods=["POST"])
def expanded(self, table_schema_id):
payload = json.loads(request.form["expanded"])
(
db.session.query(TableSchema)
.filter_by(id=table_schema_id)
.update({"expanded": payload})
)
db.session.commit()
response = json.dumps({"id": table_schema_id, "expanded": payload})
return json_success(response)
appbuilder.add_view_no_menu(TabStateView)
appbuilder.add_view_no_menu(TableSchemaView)
appbuilder.add_link(
__("Saved Queries"), href="/sqllab/my_queries/", icon="fa-save", category="SQL Lab"
)