Skip to content

Commit

Permalink
feat(SIP-95): new endpoint for table metadata (#28122)
Browse files Browse the repository at this point in the history
  • Loading branch information
betodealmeida committed Apr 25, 2024
1 parent 52f8734 commit 6cf681d
Show file tree
Hide file tree
Showing 71 changed files with 1,051 additions and 516 deletions.
5 changes: 3 additions & 2 deletions superset-frontend/src/SqlLab/actions/sqlLab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,10 +508,11 @@ describe('async actions', () => {
fetchMock.delete(updateTableSchemaEndpoint, {});
fetchMock.post(updateTableSchemaEndpoint, JSON.stringify({ id: 1 }));

const getTableMetadataEndpoint = 'glob:**/api/v1/database/*/table/*/*/';
const getTableMetadataEndpoint =
'glob:**/api/v1/database/*/table_metadata/*';
fetchMock.get(getTableMetadataEndpoint, {});
const getExtraTableMetadataEndpoint =
'glob:**/api/v1/database/*/table_metadata/extra/';
'glob:**/api/v1/database/*/table_metadata/extra/*';
fetchMock.get(getExtraTableMetadataEndpoint, {});

let isFeatureEnabledMock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ beforeEach(() => {
},
],
});
fetchMock.get('glob:*/api/v1/database/*/table/*/*', {
fetchMock.get('glob:*/api/v1/database/*/table_metadata/*', {
status: 200,
body: {
columns: table.columns,
},
});
fetchMock.get('glob:*/api/v1/database/*/table_metadata/extra/', {
fetchMock.get('glob:*/api/v1/database/*/table_metadata/extra/*', {
status: 200,
body: {},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ jest.mock(
<div data-test="mock-column-element">{column.name}</div>
),
);
const getTableMetadataEndpoint = 'glob:**/api/v1/database/*/table/*/*/';
const getTableMetadataEndpoint =
/\/api\/v1\/database\/\d+\/table_metadata\/(?:\?.*)?$/;
const getExtraTableMetadataEndpoint =
'glob:**/api/v1/database/*/table_metadata/extra/*';
/\/api\/v1\/database\/\d+\/table_metadata\/extra\/(?:\?.*)?$/;
const updateTableSchemaEndpoint = 'glob:*/tableschemaview/*/expanded';

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ const DatasetPanelWrapper = ({
const { dbId, tableName, schema } = props;
setLoading(true);
setHasColumns?.(false);
const path = `/api/v1/database/${dbId}/table/${tableName}/${schema}/`;
const path = schema
? `/api/v1/database/${dbId}/table_metadata/?name=${tableName}&schema=${schema}`
: `/api/v1/database/${dbId}/table_metadata/?name=${tableName}`;
try {
const response = await SupersetClient.get({
endpoint: path,
Expand Down
6 changes: 3 additions & 3 deletions superset-frontend/src/hooks/apiResources/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ const tableApi = api.injectEndpoints({
}),
tableMetadata: builder.query<TableMetaData, FetchTableMetadataQueryParams>({
query: ({ dbId, schema, table }) => ({
endpoint: `/api/v1/database/${dbId}/table/${encodeURIComponent(
table,
)}/${encodeURIComponent(schema)}/`,
endpoint: schema
? `/api/v1/database/${dbId}/table_metadata/?name=${table}&schema=${schema}`
: `/api/v1/database/${dbId}/table_metadata/?name=${table}`,
transformResponse: ({ json }: TableMetadataReponse) => json,
}),
}),
Expand Down
2 changes: 2 additions & 0 deletions superset/commands/database/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def run(self) -> dict[str, Any]:
datasource_names=sorted(
DatasourceName(*datasource_name)
for datasource_name in self._model.get_all_table_names_in_schema(
catalog=None,
schema=self._schema_name,
force=self._force,
cache=self._model.table_cache_enabled,
Expand All @@ -65,6 +66,7 @@ def run(self) -> dict[str, Any]:
datasource_names=sorted(
DatasourceName(*datasource_name)
for datasource_name in self._model.get_all_view_names_in_schema(
catalog=None,
schema=self._schema_name,
force=self._force,
cache=self._model.table_cache_enabled,
Expand Down
3 changes: 2 additions & 1 deletion superset/commands/database/validate_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ def run(self) -> list[dict[str, Any]]:
raise ValidatorSQLUnexpectedError()
sql = self._properties["sql"]
schema = self._properties.get("schema")
catalog = self._properties.get("catalog")
try:
timeout = current_app.config["SQLLAB_VALIDATION_TIMEOUT"]
timeout_msg = f"The query exceeded the {timeout} seconds timeout."
with utils.timeout(seconds=timeout, error_message=timeout_msg):
errors = self._validator.validate(sql, schema, self._model)
errors = self._validator.validate(sql, catalog, schema, self._model)
return [err.to_dict() for err in errors]
except Exception as ex:
logger.exception(ex)
Expand Down
13 changes: 9 additions & 4 deletions superset/commands/dataset/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from superset.daos.exceptions import DAOCreateFailedError
from superset.exceptions import SupersetSecurityException
from superset.extensions import db, security_manager
from superset.sql_parse import Table

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -61,12 +62,15 @@ def validate(self) -> None:
exceptions: list[ValidationError] = []
database_id = self._properties["database"]
table_name = self._properties["table_name"]
schema = self._properties.get("schema", None)
sql = self._properties.get("sql", None)
schema = self._properties.get("schema")
catalog = self._properties.get("catalog")

This comment has been minimized.

Copy link
@john-bodley

john-bodley May 4, 2024

Member

@betodealmeida this field doesn’t seem to exist within the associated schema.

sql = self._properties.get("sql")
owner_ids: Optional[list[int]] = self._properties.get("owners")

table = Table(table_name, schema, catalog)

# Validate uniqueness
if not DatasetDAO.validate_uniqueness(database_id, schema, table_name):
if not DatasetDAO.validate_uniqueness(database_id, table):
exceptions.append(DatasetExistsValidationError(table_name))

# Validate/Populate database
Expand All @@ -80,7 +84,7 @@ def validate(self) -> None:
if (
database
and not sql
and not DatasetDAO.validate_table_exists(database, table_name, schema)
and not DatasetDAO.validate_table_exists(database, table)
):
exceptions.append(TableNotFoundValidationError(table_name))

Expand All @@ -89,6 +93,7 @@ def validate(self) -> None:
security_manager.raise_for_access(
database=database,
sql=sql,
catalog=catalog,
schema=schema,
)
except SupersetSecurityException as ex:
Expand Down
10 changes: 8 additions & 2 deletions superset/commands/dataset/importers/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from superset.commands.exceptions import ImportFailedError
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
from superset.sql_parse import Table
from superset.utils.core import get_user

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -164,7 +165,9 @@ def import_dataset(
db.session.flush()

try:
table_exists = dataset.database.has_table_by_name(dataset.table_name)
table_exists = dataset.database.has_table(
Table(dataset.table_name, dataset.schema),
)
except Exception: # pylint: disable=broad-except
# MySQL doesn't play nice with GSheets table names
logger.warning(
Expand Down Expand Up @@ -217,7 +220,10 @@ def load_data(data_uri: str, dataset: SqlaTable, database: Database) -> None:
)
else:
logger.warning("Loading data outside the import transaction")
with database.get_sqla_engine() as engine:
with database.get_sqla_engine(
catalog=dataset.catalog,
schema=dataset.schema,
) as engine:
df.to_sql(
dataset.table_name,
con=engine,
Expand Down
4 changes: 2 additions & 2 deletions superset/commands/dataset/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from superset.daos.dataset import DatasetDAO
from superset.daos.exceptions import DAOUpdateFailedError
from superset.exceptions import SupersetSecurityException
from superset.sql_parse import Table

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -90,9 +91,8 @@ def validate(self) -> None:
# Validate uniqueness
if not DatasetDAO.validate_update_uniqueness(
self._model.database_id,
self._model.schema,
Table(table_name, self._model.schema, self._model.catalog),
self._model_id,
table_name,
):
exceptions.append(DatasetExistsValidationError(table_name))
# Validate/Populate database not allowed to change
Expand Down
25 changes: 19 additions & 6 deletions superset/commands/sql_lab/estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from __future__ import annotations

import logging
from typing import Any
from typing import Any, TypedDict

from flask_babel import gettext as __

Expand All @@ -27,7 +27,6 @@
from superset.exceptions import SupersetErrorException, SupersetTimeoutException
from superset.jinja_context import get_template_processor
from superset.models.core import Database
from superset.sqllab.schemas import EstimateQueryCostSchema
from superset.utils import core as utils

config = app.config
Expand All @@ -37,18 +36,28 @@
logger = logging.getLogger(__name__)


class EstimateQueryCostType(TypedDict):
database_id: int
sql: str
template_params: dict[str, Any]
catalog: str | None
schema: str | None


class QueryEstimationCommand(BaseCommand):
_database_id: int
_sql: str
_template_params: dict[str, Any]
_schema: str
_database: Database
_catalog: str | None

def __init__(self, params: EstimateQueryCostSchema) -> None:
self._database_id = params.get("database_id")
def __init__(self, params: EstimateQueryCostType) -> None:
self._database_id = params["database_id"]
self._sql = params.get("sql", "")
self._template_params = params.get("template_params", {})
self._schema = params.get("schema", "")
self._schema = params.get("schema") or ""
self._catalog = params.get("catalog")

def validate(self) -> None:
self._database = db.session.query(Database).get(self._database_id)
Expand Down Expand Up @@ -77,7 +86,11 @@ def run(
try:
with utils.timeout(seconds=timeout, error_message=timeout_msg):
cost = self._database.db_engine_spec.estimate_query_cost(
self._database, self._schema, sql, utils.QuerySource.SQL_LAB
self._database,
self._catalog,
self._schema,
sql,
utils.QuerySource.SQL_LAB,
)
except SupersetTimeoutException as ex:
logger.exception(ex)
Expand Down

0 comments on commit 6cf681d

Please sign in to comment.