sql_lab.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. # Licensed to the Apache Software Foundation (ASF) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The ASF licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. from typing import Callable
  18. import simplejson as json
  19. from flask import g, redirect, request, Response
  20. from flask_appbuilder import expose
  21. from flask_appbuilder.models.sqla.interface import SQLAInterface
  22. from flask_appbuilder.security.decorators import has_access, has_access_api
  23. from flask_babel import lazy_gettext as _
  24. from flask_sqlalchemy import BaseQuery
  25. from superset import db, get_feature_flags, security_manager
  26. from superset.constants import RouteMethod
  27. from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState
  28. from superset.utils import core as utils
  29. from .base import (
  30. BaseFilter,
  31. BaseSupersetView,
  32. DeleteMixin,
  33. json_success,
  34. SupersetModelView,
  35. )
  36. class QueryFilter(BaseFilter): # pylint: disable=too-few-public-methods
  37. def apply(self, query: BaseQuery, value: Callable) -> BaseQuery:
  38. """
  39. Filter queries to only those owned by current user. If
  40. can_access_all_queries permission is set a user can list all queries
  41. :returns: query
  42. """
  43. if not security_manager.can_access_all_queries():
  44. query = query.filter(Query.user_id == g.user.get_user_id())
  45. return query
  46. class QueryView(SupersetModelView):
  47. datamodel = SQLAInterface(Query)
  48. include_route_methods = {RouteMethod.SHOW, RouteMethod.LIST, RouteMethod.API_READ}
  49. list_title = _("List Query")
  50. show_title = _("Show Query")
  51. add_title = _("Add Query")
  52. edit_title = _("Edit Query")
  53. list_columns = ["username", "database_name", "status", "start_time", "end_time"]
  54. order_columns = ["status", "start_time", "end_time"]
  55. base_filters = [["id", QueryFilter, lambda: []]]
  56. label_columns = {
  57. "user": _("User"),
  58. "username": _("User"),
  59. "database_name": _("Database"),
  60. "status": _("Status"),
  61. "start_time": _("Start Time"),
  62. "end_time": _("End Time"),
  63. }
  64. class SavedQueryView(
  65. SupersetModelView, DeleteMixin
  66. ): # pylint: disable=too-many-ancestors
  67. datamodel = SQLAInterface(SavedQuery)
  68. include_route_methods = RouteMethod.CRUD_SET
  69. list_title = _("List Saved Query")
  70. show_title = _("Show Saved Query")
  71. add_title = _("Add Saved Query")
  72. edit_title = _("Edit Saved Query")
  73. list_columns = [
  74. "label",
  75. "user",
  76. "database",
  77. "schema",
  78. "description",
  79. "modified",
  80. "pop_tab_link",
  81. ]
  82. order_columns = ["label", "schema", "description", "modified"]
  83. show_columns = [
  84. "id",
  85. "label",
  86. "user",
  87. "database",
  88. "description",
  89. "sql",
  90. "pop_tab_link",
  91. ]
  92. search_columns = ("label", "user", "database", "schema", "changed_on")
  93. add_columns = ["label", "database", "description", "sql"]
  94. edit_columns = add_columns
  95. base_order = ("changed_on", "desc")
  96. label_columns = {
  97. "label": _("Label"),
  98. "user": _("User"),
  99. "database": _("Database"),
  100. "description": _("Description"),
  101. "modified": _("Modified"),
  102. "end_time": _("End Time"),
  103. "pop_tab_link": _("Pop Tab Link"),
  104. "changed_on": _("Changed on"),
  105. }
  106. show_template = "superset/models/savedquery/show.html"
  107. def pre_add(self, item):
  108. item.user = g.user
  109. def pre_update(self, item):
  110. self.pre_add(item)
  111. @has_access
  112. @expose("show/<pk>")
  113. def show(self, pk):
  114. pk = self._deserialize_pk_if_composite(pk)
  115. widgets = self._show(pk)
  116. query = self.datamodel.get(pk).to_json()
  117. query["extra_json"] = json.loads(query["extra_json"])
  118. payload = {"common": {"feature_flags": get_feature_flags(), "query": query}}
  119. return self.render_template(
  120. self.show_template,
  121. pk=pk,
  122. title=self.show_title,
  123. widgets=widgets,
  124. related_views=self._related_views,
  125. bootstrap_data=json.dumps(payload, default=utils.json_iso_dttm_ser),
  126. )
  127. class SavedQueryViewApi(SavedQueryView): # pylint: disable=too-many-ancestors
  128. include_route_methods = {
  129. RouteMethod.API_READ,
  130. RouteMethod.API_CREATE,
  131. RouteMethod.API_UPDATE,
  132. RouteMethod.API_GET,
  133. }
  134. list_columns = [
  135. "id",
  136. "label",
  137. "sqlalchemy_uri",
  138. "user_email",
  139. "schema",
  140. "description",
  141. "sql",
  142. "extra_json",
  143. ]
  144. add_columns = ["label", "db_id", "schema", "description", "sql", "extra_json"]
  145. edit_columns = add_columns
  146. show_columns = add_columns + ["id"]
  147. @has_access_api
  148. @expose("show/<pk>")
  149. def show(self, pk):
  150. return super().show(pk)
  151. def _get_owner_id(tab_state_id):
  152. return db.session.query(TabState.user_id).filter_by(id=tab_state_id).scalar()
  153. class TabStateView(BaseSupersetView):
  154. @has_access_api
  155. @expose("/", methods=["POST"])
  156. def post(self): # pylint: disable=no-self-use
  157. query_editor = json.loads(request.form["queryEditor"])
  158. tab_state = TabState(
  159. user_id=g.user.get_id(),
  160. label=query_editor.get("title", "Untitled Query"),
  161. active=True,
  162. database_id=query_editor["dbId"],
  163. schema=query_editor.get("schema"),
  164. sql=query_editor.get("sql", "SELECT ..."),
  165. query_limit=query_editor.get("queryLimit"),
  166. )
  167. (
  168. db.session.query(TabState)
  169. .filter_by(user_id=g.user.get_id())
  170. .update({"active": False})
  171. )
  172. db.session.add(tab_state)
  173. db.session.commit()
  174. return json_success(json.dumps({"id": tab_state.id}))
  175. @has_access_api
  176. @expose("/<int:tab_state_id>", methods=["DELETE"])
  177. def delete(self, tab_state_id): # pylint: disable=no-self-use
  178. if _get_owner_id(tab_state_id) != int(g.user.get_id()):
  179. return Response(status=403)
  180. db.session.query(TabState).filter(TabState.id == tab_state_id).delete(
  181. synchronize_session=False
  182. )
  183. db.session.query(TableSchema).filter(
  184. TableSchema.tab_state_id == tab_state_id
  185. ).delete(synchronize_session=False)
  186. db.session.commit()
  187. return json_success(json.dumps("OK"))
  188. @has_access_api
  189. @expose("/<int:tab_state_id>", methods=["GET"])
  190. def get(self, tab_state_id): # pylint: disable=no-self-use
  191. if _get_owner_id(tab_state_id) != int(g.user.get_id()):
  192. return Response(status=403)
  193. tab_state = db.session.query(TabState).filter_by(id=tab_state_id).first()
  194. if tab_state is None:
  195. return Response(status=404)
  196. return json_success(
  197. json.dumps(tab_state.to_dict(), default=utils.json_iso_dttm_ser)
  198. )
  199. @has_access_api
  200. @expose("<int:tab_state_id>/activate", methods=["POST"])
  201. def activate(self, tab_state_id): # pylint: disable=no-self-use
  202. owner_id = _get_owner_id(tab_state_id)
  203. if owner_id is None:
  204. return Response(status=404)
  205. if owner_id != int(g.user.get_id()):
  206. return Response(status=403)
  207. (
  208. db.session.query(TabState)
  209. .filter_by(user_id=g.user.get_id())
  210. .update({"active": TabState.id == tab_state_id})
  211. )
  212. db.session.commit()
  213. return json_success(json.dumps(tab_state_id))
  214. @has_access_api
  215. @expose("<int:tab_state_id>", methods=["PUT"])
  216. def put(self, tab_state_id): # pylint: disable=no-self-use
  217. if _get_owner_id(tab_state_id) != int(g.user.get_id()):
  218. return Response(status=403)
  219. fields = {k: json.loads(v) for k, v in request.form.to_dict().items()}
  220. db.session.query(TabState).filter_by(id=tab_state_id).update(fields)
  221. db.session.commit()
  222. return json_success(json.dumps(tab_state_id))
  223. @has_access_api
  224. @expose("<int:tab_state_id>/migrate_query", methods=["POST"])
  225. def migrate_query(self, tab_state_id): # pylint: disable=no-self-use
  226. if _get_owner_id(tab_state_id) != int(g.user.get_id()):
  227. return Response(status=403)
  228. client_id = json.loads(request.form["queryId"])
  229. db.session.query(Query).filter_by(client_id=client_id).update(
  230. {"sql_editor_id": tab_state_id}
  231. )
  232. db.session.commit()
  233. return json_success(json.dumps(tab_state_id))
  234. @has_access_api
  235. @expose("<int:tab_state_id>/query/<client_id>", methods=["DELETE"])
  236. def delete_query(self, tab_state_id, client_id): # pylint: disable=no-self-use
  237. db.session.query(Query).filter_by(
  238. client_id=client_id, user_id=g.user.get_id(), sql_editor_id=tab_state_id
  239. ).delete(synchronize_session=False)
  240. db.session.commit()
  241. return json_success(json.dumps("OK"))
  242. class TableSchemaView(BaseSupersetView):
  243. @has_access_api
  244. @expose("/", methods=["POST"])
  245. def post(self): # pylint: disable=no-self-use
  246. table = json.loads(request.form["table"])
  247. # delete any existing table schema
  248. db.session.query(TableSchema).filter(
  249. TableSchema.tab_state_id == table["queryEditorId"],
  250. TableSchema.database_id == table["dbId"],
  251. TableSchema.schema == table["schema"],
  252. TableSchema.table == table["name"],
  253. ).delete(synchronize_session=False)
  254. table_schema = TableSchema(
  255. tab_state_id=table["queryEditorId"],
  256. database_id=table["dbId"],
  257. schema=table["schema"],
  258. table=table["name"],
  259. description=json.dumps(table),
  260. expanded=True,
  261. )
  262. db.session.add(table_schema)
  263. db.session.commit()
  264. return json_success(json.dumps({"id": table_schema.id}))
  265. @has_access_api
  266. @expose("/<int:table_schema_id>", methods=["DELETE"])
  267. def delete(self, table_schema_id): # pylint: disable=no-self-use
  268. db.session.query(TableSchema).filter(TableSchema.id == table_schema_id).delete(
  269. synchronize_session=False
  270. )
  271. db.session.commit()
  272. return json_success(json.dumps("OK"))
  273. @has_access_api
  274. @expose("/<int:table_schema_id>/expanded", methods=["POST"])
  275. def expanded(self, table_schema_id): # pylint: disable=no-self-use
  276. payload = json.loads(request.form["expanded"])
  277. (
  278. db.session.query(TableSchema)
  279. .filter_by(id=table_schema_id)
  280. .update({"expanded": payload})
  281. )
  282. db.session.commit()
  283. response = json.dumps({"id": table_schema_id, "expanded": payload})
  284. return json_success(response)
  285. class SqlLab(BaseSupersetView):
  286. """The base views for Superset!"""
  287. @expose("/my_queries/")
  288. @has_access
  289. def my_queries(self): # pylint: disable=no-self-use
  290. """Assigns a list of found users to the given role."""
  291. return redirect("/savedqueryview/list/?_flt_0_user={}".format(g.user.id))