Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[database] Fix, tables endpoint #9144

Merged
merged 14 commits into from
Feb 20, 2020
71 changes: 34 additions & 37 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,7 @@
import pandas as pd
import pyarrow as pa
import simplejson as json
from flask import (
abort,
flash,
g,
Markup,
redirect,
render_template,
request,
Response,
url_for,
)
from flask import abort, flash, g, Markup, redirect, render_template, 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
Expand All @@ -46,7 +36,6 @@
from sqlalchemy import and_, Integer, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.session import Session
from werkzeug.routing import BaseConverter
from werkzeug.urls import Href

import superset.models.core as models
Expand Down Expand Up @@ -88,11 +77,10 @@
from superset.utils import core as utils, dashboard_import_export
from superset.utils.dates import now_as_float
from superset.utils.decorators import etag_cache, stats_timing
from superset.views.chart import views as chart_views
from superset.views.database.filters import DatabaseFilter

from .base import (
api,
BaseFilter,
BaseSupersetView,
check_ownership,
common_bootstrap_payload,
Expand All @@ -107,8 +95,6 @@
json_success,
SupersetModelView,
)
from .dashboard import views as dash_views
from .database import views as in_views
from .utils import (
apply_display_max_row_limit,
bootstrap_user_data,
Expand Down Expand Up @@ -1068,30 +1054,39 @@ def schemas(self, db_id, force_refresh="false"):

@api
@has_access_api
@expose("/tables/<db_id>/<schema>/<substr>/")
@expose("/tables/<db_id>/<schema>/<substr>/<force_refresh>/")
def tables(self, db_id, schema, substr, force_refresh="false"):
@expose("/tables/<int:db_id>/<schema>/<substr>/")
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
def tables(
self, db_id: int, schema: str, substr: str, force_refresh: str = "false"
):
"""Endpoint to fetch the list of tables for given database"""
db_id = int(db_id)
force_refresh = force_refresh.lower() == "true"
schema = utils.parse_js_uri_path_item(schema, eval_undefined=True)
substr = utils.parse_js_uri_path_item(substr, eval_undefined=True)
database = db.session.query(models.Database).filter_by(id=db_id).one()
# Guarantees database filtering by security access
query = db.session.query(models.Database)
query = DatabaseFilter("id", SQLAInterface(models.Database, db.session)).apply(
query, None
)
database = query.filter_by(id=db_id).one_or_none()
if not database:
return json_error_response("Not found", 404)
Comment on lines +1068 to +1070
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we settle for try-except with .one() or .one_or_none() with if not? I'm fine either way, just prefer to have it as uniform as possible in the codebase.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a majority of one_or_none() on core.py


if schema:
force_refresh_parsed = force_refresh.lower() == "true"
schema_parsed = utils.parse_js_uri_path_item(schema, eval_undefined=True)
substr_parsed = utils.parse_js_uri_path_item(substr, eval_undefined=True)

if schema_parsed:
tables = (
database.get_all_table_names_in_schema(
schema=schema,
force=force_refresh,
schema=schema_parsed,
force=force_refresh_parsed,
cache=database.table_cache_enabled,
cache_timeout=database.table_cache_timeout,
)
or []
)
views = (
database.get_all_view_names_in_schema(
schema=schema,
force=force_refresh,
schema=schema_parsed,
force=force_refresh_parsed,
cache=database.table_cache_enabled,
cache_timeout=database.table_cache_timeout,
)
Expand All @@ -1105,20 +1100,22 @@ def tables(self, db_id, schema, substr, force_refresh="false"):
cache=True, force=False, cache_timeout=24 * 60 * 60
)
tables = security_manager.get_datasources_accessible_by_user(
database, tables, schema
database, tables, schema_parsed
)
views = security_manager.get_datasources_accessible_by_user(
database, views, schema
database, views, schema_parsed
)

def get_datasource_label(ds_name: utils.DatasourceName) -> str:
return ds_name.table if schema else f"{ds_name.schema}.{ds_name.table}"
return (
ds_name.table if schema_parsed else f"{ds_name.schema}.{ds_name.table}"
)

if substr:
tables = [tn for tn in tables if substr in get_datasource_label(tn)]
views = [vn for vn in views if substr in get_datasource_label(vn)]
if substr_parsed:
tables = [tn for tn in tables if substr_parsed in get_datasource_label(tn)]
views = [vn for vn in views if substr_parsed in get_datasource_label(vn)]

if not schema and database.default_schemas:
if not schema_parsed and database.default_schemas:
user_schema = g.user.email.split("@")[0]
valid_schemas = set(database.default_schemas + [user_schema])

Expand All @@ -1129,7 +1126,7 @@ def get_datasource_label(ds_name: utils.DatasourceName) -> str:
total_items = len(tables) + len(views)
max_tables = len(tables)
max_views = len(views)
if total_items and substr:
if total_items and substr_parsed:
max_tables = max_items * len(tables) // total_items
max_views = max_items * len(views) // total_items

Expand Down
2 changes: 2 additions & 0 deletions superset/views/database/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ class DatabaseRestApi(DatabaseMixin, BaseSupersetModelRestApi):
max_page_size = -1
validators_columns = {"sqlalchemy_uri": sqlalchemy_uri_validator}

openapi_spec_tag = "Database"

@expose(
"/<int:pk>/table/<string:table_name>/<string:schema_name>/", methods=["GET"]
)
Expand Down
7 changes: 7 additions & 0 deletions tests/base_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@


class SupersetTestCase(TestCase):

default_schema_backend_map = {
"sqlite": "main",
"mysql": "superset",
"postgresql": "public",
}

def __init__(self, *args, **kwargs):
super(SupersetTestCase, self).__init__(*args, **kwargs)
self.maxDiff = None
Expand Down
37 changes: 37 additions & 0 deletions tests/core_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,43 @@ def test_cache_key_changes_when_datasource_is_updated(self):
# the new cache_key should be different due to updated datasource
self.assertNotEqual(cache_key_original, cache_key_new)

def test_get_superset_tables_not_allowed(self):
example_db = utils.get_example_database()
schema_name = self.default_schema_backend_map[example_db.backend]
self.login(username="gamma")
uri = f"superset/tables/{example_db.id}/{schema_name}/undefined/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)

def test_get_superset_tables_substr(self):
example_db = utils.get_example_database()
self.login(username="admin")
schema_name = self.default_schema_backend_map[example_db.backend]
uri = f"superset/tables/{example_db.id}/{schema_name}/ab_role/"
rv = self.client.get(uri)
response = json.loads(rv.data.decode("utf-8"))
self.assertEqual(rv.status_code, 200)

expeted_response = {
"options": [
{
"label": "ab_role",
"schema": schema_name,
"title": "ab_role",
"type": "table",
"value": "ab_role",
}
],
"tableLength": 1,
}
self.assertEqual(response, expeted_response)

def test_get_superset_tables_not_found(self):
self.login(username="admin")
uri = f"superset/tables/invalid/public/undefined/"
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 404)

def test_api_v1_query_endpoint(self):
self.login(username="admin")
qc_dict = self._get_query_context_dict()
Expand Down