mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
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:
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user