api.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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 Any, Dict, List, Optional
  18. from flask_appbuilder.api import expose, protect, safe
  19. from flask_appbuilder.models.sqla.interface import SQLAInterface
  20. from sqlalchemy.exc import NoSuchTableError, SQLAlchemyError
  21. from superset import event_logger
  22. from superset.models.core import Database
  23. from superset.utils.core import error_msg_from_exception
  24. from superset.views.base_api import BaseSupersetModelRestApi
  25. from superset.views.database.decorators import check_datasource_access
  26. from superset.views.database.filters import DatabaseFilter
  27. from superset.views.database.mixins import DatabaseMixin
  28. from superset.views.database.validators import sqlalchemy_uri_validator
  29. def get_foreign_keys_metadata(
  30. database: Database, table_name: str, schema_name: Optional[str]
  31. ) -> List[Dict[str, Any]]:
  32. foreign_keys = database.get_foreign_keys(table_name, schema_name)
  33. for fk in foreign_keys:
  34. fk["column_names"] = fk.pop("constrained_columns")
  35. fk["type"] = "fk"
  36. return foreign_keys
  37. def get_indexes_metadata(
  38. database: Database, table_name: str, schema_name: Optional[str]
  39. ) -> List[Dict[str, Any]]:
  40. indexes = database.get_indexes(table_name, schema_name)
  41. for idx in indexes:
  42. idx["type"] = "index"
  43. return indexes
  44. def get_col_type(col: Dict) -> str:
  45. try:
  46. dtype = f"{col['type']}"
  47. except Exception: # pylint: disable=broad-except
  48. # sqla.types.JSON __str__ has a bug, so using __class__.
  49. dtype = col["type"].__class__.__name__
  50. return dtype
  51. def get_table_metadata(
  52. database: Database, table_name: str, schema_name: Optional[str]
  53. ) -> Dict:
  54. """
  55. Get table metadata information, including type, pk, fks.
  56. This function raises SQLAlchemyError when a schema is not found.
  57. :param database: The database model
  58. :param table_name: Table name
  59. :param schema_name: schema name
  60. :return: Dict table metadata ready for API response
  61. """
  62. keys: List = []
  63. columns = database.get_columns(table_name, schema_name)
  64. primary_key = database.get_pk_constraint(table_name, schema_name)
  65. if primary_key and primary_key.get("constrained_columns"):
  66. primary_key["column_names"] = primary_key.pop("constrained_columns")
  67. primary_key["type"] = "pk"
  68. keys += [primary_key]
  69. foreign_keys = get_foreign_keys_metadata(database, table_name, schema_name)
  70. indexes = get_indexes_metadata(database, table_name, schema_name)
  71. keys += foreign_keys + indexes
  72. payload_columns: List[Dict] = []
  73. for col in columns:
  74. dtype = get_col_type(col)
  75. payload_columns.append(
  76. {
  77. "name": col["name"],
  78. "type": dtype.split("(")[0] if "(" in dtype else dtype,
  79. "longType": dtype,
  80. "keys": [k for k in keys if col["name"] in k.get("column_names")],
  81. }
  82. )
  83. return {
  84. "name": table_name,
  85. "columns": payload_columns,
  86. "selectStar": database.select_star(
  87. table_name,
  88. schema=schema_name,
  89. show_cols=True,
  90. indent=True,
  91. cols=columns,
  92. latest_partition=True,
  93. ),
  94. "primaryKey": primary_key,
  95. "foreignKeys": foreign_keys,
  96. "indexes": keys,
  97. }
  98. class DatabaseRestApi(DatabaseMixin, BaseSupersetModelRestApi):
  99. datamodel = SQLAInterface(Database)
  100. include_route_methods = {"get_list", "table_metadata", "select_star"}
  101. class_permission_name = "DatabaseView"
  102. method_permission_name = {
  103. "get_list": "list",
  104. "table_metadata": "list",
  105. "select_star": "list",
  106. }
  107. resource_name = "database"
  108. allow_browser_login = True
  109. base_filters = [["id", DatabaseFilter, lambda: []]]
  110. list_columns = [
  111. "id",
  112. "database_name",
  113. "expose_in_sqllab",
  114. "allow_ctas",
  115. "force_ctas_schema",
  116. "allow_run_async",
  117. "allow_dml",
  118. "allow_multi_schema_metadata_fetch",
  119. "allow_csv_upload",
  120. "allows_subquery",
  121. "allows_cost_estimate",
  122. "backend",
  123. "function_names",
  124. ]
  125. show_columns = list_columns
  126. # Removes the local limit for the page size
  127. max_page_size = -1
  128. validators_columns = {"sqlalchemy_uri": sqlalchemy_uri_validator}
  129. openapi_spec_tag = "Database"
  130. @expose(
  131. "/<int:pk>/table/<string:table_name>/<string:schema_name>/", methods=["GET"]
  132. )
  133. @protect()
  134. @check_datasource_access
  135. @safe
  136. @event_logger.log_this
  137. def table_metadata(self, database: Database, table_name: str, schema_name: str):
  138. """ Table schema info
  139. ---
  140. get:
  141. description: Get database table metadata
  142. parameters:
  143. - in: path
  144. schema:
  145. type: integer
  146. name: pk
  147. description: The database id
  148. - in: path
  149. schema:
  150. type: string
  151. name: table_name
  152. description: Table name
  153. - in: path
  154. schema:
  155. type: string
  156. name: schema
  157. description: Table schema
  158. responses:
  159. 200:
  160. description: Table schema info
  161. content:
  162. text/plain:
  163. schema:
  164. type: object
  165. properties:
  166. columns:
  167. type: array
  168. description: Table columns info
  169. items:
  170. type: object
  171. properties:
  172. keys:
  173. type: array
  174. items:
  175. type: string
  176. longType:
  177. type: string
  178. name:
  179. type: string
  180. type:
  181. type: string
  182. foreignKeys:
  183. type: array
  184. description: Table list of foreign keys
  185. items:
  186. type: object
  187. properties:
  188. column_names:
  189. type: array
  190. items:
  191. type: string
  192. name:
  193. type: string
  194. options:
  195. type: object
  196. referred_columns:
  197. type: array
  198. items:
  199. type: string
  200. referred_schema:
  201. type: string
  202. referred_table:
  203. type: string
  204. type:
  205. type: string
  206. indexes:
  207. type: array
  208. description: Table list of indexes
  209. items:
  210. type: object
  211. properties:
  212. column_names:
  213. type: array
  214. items:
  215. type: string
  216. name:
  217. type: string
  218. options:
  219. type: object
  220. referred_columns:
  221. type: array
  222. items:
  223. type: string
  224. referred_schema:
  225. type: string
  226. referred_table:
  227. type: string
  228. type:
  229. type: string
  230. primaryKey:
  231. type: object
  232. properties:
  233. column_names:
  234. type: array
  235. items:
  236. type: string
  237. name:
  238. type: string
  239. type:
  240. type: string
  241. 400:
  242. $ref: '#/components/responses/400'
  243. 401:
  244. $ref: '#/components/responses/401'
  245. 404:
  246. $ref: '#/components/responses/404'
  247. 422:
  248. $ref: '#/components/responses/422'
  249. 500:
  250. $ref: '#/components/responses/500'
  251. """
  252. self.incr_stats("init", self.table_metadata.__name__)
  253. try:
  254. table_info: Dict = get_table_metadata(database, table_name, schema_name)
  255. except SQLAlchemyError as e:
  256. self.incr_stats("error", self.table_metadata.__name__)
  257. return self.response_422(error_msg_from_exception(e))
  258. self.incr_stats("success", self.table_metadata.__name__)
  259. return self.response(200, **table_info)
  260. @expose("/<int:pk>/select_star/<string:table_name>/", methods=["GET"])
  261. @expose(
  262. "/<int:pk>/select_star/<string:table_name>/<string:schema_name>/",
  263. methods=["GET"],
  264. )
  265. @protect()
  266. @check_datasource_access
  267. @safe
  268. @event_logger.log_this
  269. def select_star(
  270. self, database: Database, table_name: str, schema_name: Optional[str] = None
  271. ):
  272. """ Table schema info
  273. ---
  274. get:
  275. description: Get database select star for table
  276. parameters:
  277. - in: path
  278. schema:
  279. type: integer
  280. name: pk
  281. description: The database id
  282. - in: path
  283. schema:
  284. type: string
  285. name: table_name
  286. description: Table name
  287. - in: path
  288. schema:
  289. type: string
  290. name: schema_name
  291. description: Table schema
  292. responses:
  293. 200:
  294. description: select star for table
  295. content:
  296. text/plain:
  297. schema:
  298. type: object
  299. properties:
  300. result:
  301. type: string
  302. description: SQL select star
  303. 400:
  304. $ref: '#/components/responses/400'
  305. 401:
  306. $ref: '#/components/responses/401'
  307. 404:
  308. $ref: '#/components/responses/404'
  309. 422:
  310. $ref: '#/components/responses/422'
  311. 500:
  312. $ref: '#/components/responses/500'
  313. """
  314. self.incr_stats("init", self.select_star.__name__)
  315. try:
  316. result = database.select_star(
  317. table_name, schema_name, latest_partition=True, show_cols=True
  318. )
  319. except NoSuchTableError:
  320. self.incr_stats("error", self.select_star.__name__)
  321. return self.response(404, message="Table not found on the database")
  322. self.incr_stats("success", self.select_star.__name__)
  323. return self.response(200, result=result)