mirror of
https://github.com/apache/superset.git
synced 2026-07-24 23:56:22 +00:00
Compare commits
1 Commits
fix/semant
...
fix-datase
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602bd657bb |
@@ -276,6 +276,7 @@ class ImportV1ColumnSchema(Schema):
|
||||
description = fields.String(allow_none=True)
|
||||
python_date_format = fields.String(allow_none=True)
|
||||
datetime_format = fields.String(allow_none=True)
|
||||
uuid = fields.UUID(allow_none=True)
|
||||
|
||||
|
||||
class ImportMetricCurrencySchema(Schema):
|
||||
@@ -318,6 +319,7 @@ class ImportV1MetricSchema(Schema):
|
||||
currency = CurrencyField(ImportMetricCurrencySchema, allow_none=True)
|
||||
extra = fields.Dict(allow_none=True)
|
||||
warning_text = fields.String(allow_none=True)
|
||||
uuid = fields.UUID(allow_none=True)
|
||||
|
||||
|
||||
class ImportV1DatasetSchema(Schema):
|
||||
|
||||
@@ -615,6 +615,7 @@ class ImportExportMixin(UUIDMixin):
|
||||
recursive=recursive,
|
||||
include_parent_ref=include_parent_ref,
|
||||
include_defaults=include_defaults,
|
||||
export_uuids=export_uuids,
|
||||
)
|
||||
for child in getattr(self, cld)
|
||||
],
|
||||
|
||||
@@ -95,6 +95,14 @@ class TestExportDatasetsCommand(SupersetTestCase):
|
||||
type_map = {
|
||||
column.column_name: str(column.type) for column in example_dataset.columns
|
||||
}
|
||||
# column/metric UUIDs are now exported so folder references survive an
|
||||
# import; they are assigned dynamically, so build lookups by name.
|
||||
column_uuid_map = {
|
||||
column.column_name: str(column.uuid) for column in example_dataset.columns
|
||||
}
|
||||
metric_uuid_map = {
|
||||
metric.metric_name: str(metric.uuid) for metric in example_dataset.metrics
|
||||
}
|
||||
|
||||
assert metadata == {
|
||||
"cache_timeout": None,
|
||||
@@ -115,6 +123,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
|
||||
"advanced_data_type": None,
|
||||
"verbose_name": None,
|
||||
"extra": None,
|
||||
"uuid": column_uuid_map["source"],
|
||||
},
|
||||
{
|
||||
"column_name": "target",
|
||||
@@ -127,6 +136,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
|
||||
"is_dttm": False,
|
||||
"python_date_format": None,
|
||||
"type": type_map["target"],
|
||||
"uuid": column_uuid_map["target"],
|
||||
"advanced_data_type": None,
|
||||
"verbose_name": None,
|
||||
"extra": None,
|
||||
@@ -145,6 +155,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
|
||||
"advanced_data_type": None,
|
||||
"verbose_name": None,
|
||||
"extra": None,
|
||||
"uuid": column_uuid_map["value"],
|
||||
},
|
||||
],
|
||||
"database_uuid": str(example_db.uuid),
|
||||
@@ -165,6 +176,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
|
||||
"metric_type": "count",
|
||||
"verbose_name": "COUNT(*)",
|
||||
"warning_text": None,
|
||||
"uuid": metric_uuid_map["count"],
|
||||
},
|
||||
{
|
||||
"currency": None,
|
||||
@@ -176,6 +188,7 @@ class TestExportDatasetsCommand(SupersetTestCase):
|
||||
"metric_type": None,
|
||||
"verbose_name": None,
|
||||
"warning_text": None,
|
||||
"uuid": metric_uuid_map["sum__value"],
|
||||
},
|
||||
],
|
||||
"folders": None,
|
||||
|
||||
@@ -41,10 +41,27 @@ def test_export(session: Session) -> None:
|
||||
db.session.flush()
|
||||
|
||||
columns = [
|
||||
TableColumn(column_name="ds", is_dttm=1, type="TIMESTAMP"),
|
||||
TableColumn(column_name="user_id", type="INTEGER"),
|
||||
TableColumn(column_name="revenue", type="INTEGER"),
|
||||
TableColumn(column_name="expenses", type="INTEGER"),
|
||||
TableColumn(
|
||||
column_name="ds",
|
||||
is_dttm=1,
|
||||
type="TIMESTAMP",
|
||||
uuid=UUID("00000000-0000-0000-0000-000000000006"),
|
||||
),
|
||||
TableColumn(
|
||||
column_name="user_id",
|
||||
type="INTEGER",
|
||||
uuid=UUID("00000000-0000-0000-0000-000000000007"),
|
||||
),
|
||||
TableColumn(
|
||||
column_name="revenue",
|
||||
type="INTEGER",
|
||||
uuid=UUID("00000000-0000-0000-0000-000000000008"),
|
||||
),
|
||||
TableColumn(
|
||||
column_name="expenses",
|
||||
type="INTEGER",
|
||||
uuid=UUID("00000000-0000-0000-0000-000000000009"),
|
||||
),
|
||||
TableColumn(
|
||||
column_name="profit",
|
||||
type="INTEGER",
|
||||
@@ -211,6 +228,7 @@ metrics:
|
||||
extra:
|
||||
warning_markdown: null
|
||||
warning_text: null
|
||||
uuid: 00000000-0000-0000-0000-000000000004
|
||||
columns:
|
||||
- column_name: profit
|
||||
verbose_name: null
|
||||
@@ -226,6 +244,7 @@ columns:
|
||||
datetime_format: null
|
||||
extra:
|
||||
certified_by: User
|
||||
uuid: 00000000-0000-0000-0000-000000000005
|
||||
- column_name: ds
|
||||
verbose_name: null
|
||||
is_dttm: 1
|
||||
@@ -239,6 +258,7 @@ columns:
|
||||
python_date_format: null
|
||||
datetime_format: null
|
||||
extra: null
|
||||
uuid: 00000000-0000-0000-0000-000000000006
|
||||
- column_name: user_id
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
@@ -252,19 +272,7 @@ columns:
|
||||
python_date_format: null
|
||||
datetime_format: null
|
||||
extra: null
|
||||
- column_name: expenses
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: INTEGER
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
datetime_format: null
|
||||
extra: null
|
||||
uuid: 00000000-0000-0000-0000-000000000007
|
||||
- column_name: revenue
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
@@ -278,6 +286,21 @@ columns:
|
||||
python_date_format: null
|
||||
datetime_format: null
|
||||
extra: null
|
||||
uuid: 00000000-0000-0000-0000-000000000008
|
||||
- column_name: expenses
|
||||
verbose_name: null
|
||||
is_dttm: false
|
||||
is_active: true
|
||||
type: INTEGER
|
||||
advanced_data_type: null
|
||||
groupby: true
|
||||
filterable: true
|
||||
expression: null
|
||||
description: null
|
||||
python_date_format: null
|
||||
datetime_format: null
|
||||
extra: null
|
||||
uuid: 00000000-0000-0000-0000-000000000009
|
||||
version: 1.0.0
|
||||
database_uuid: {database.uuid}
|
||||
""",
|
||||
|
||||
@@ -26,6 +26,7 @@ from unittest.mock import Mock, patch
|
||||
from urllib import request
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from flask import current_app
|
||||
from flask_appbuilder.security.sqla.models import Role, User
|
||||
from pytest_mock import MockerFixture
|
||||
@@ -253,6 +254,110 @@ def test_import_dataset(mocker: MockerFixture, session: Session) -> None:
|
||||
assert sqla_table.database.id == database.id
|
||||
|
||||
|
||||
def test_export_import_round_trip_preserves_metric_folder_membership(
|
||||
mocker: MockerFixture, session: Session
|
||||
) -> None:
|
||||
"""
|
||||
A metric (or column) assigned to a custom folder must stay in that folder
|
||||
after the dataset is exported and imported into another workspace.
|
||||
|
||||
Folder leaves reference metrics/columns by UUID. If the export drops the
|
||||
metric/column UUIDs, the importer recreates them with fresh random UUIDs
|
||||
while the ``folders`` JSON still points at the originals — so the metric can
|
||||
no longer be matched to its folder and is re-homed to the default folder.
|
||||
This exercises the full export -> import round trip and asserts the
|
||||
imported metric/column keep the UUIDs the folder leaves reference.
|
||||
"""
|
||||
from superset.commands.dataset.export import ExportDatasetsCommand
|
||||
from superset.connectors.sqla.models import SqlMetric
|
||||
|
||||
mocker.patch.object(security_manager, "can_access", return_value=True)
|
||||
|
||||
engine = db.session.get_bind()
|
||||
SqlaTable.metadata.create_all(engine) # pylint: disable=no-member
|
||||
|
||||
# --- source workspace: a dataset with a metric + column pinned to a custom
|
||||
# folder, referenced by UUID ---
|
||||
source_db = Database(database_name="source_db", sqlalchemy_uri="sqlite://")
|
||||
db.session.add(source_db)
|
||||
db.session.flush()
|
||||
|
||||
metric_uuid = uuid.UUID("00000000-0000-0000-0000-0000000000a1")
|
||||
column_uuid = uuid.UUID("00000000-0000-0000-0000-0000000000b2")
|
||||
folder_uuid = uuid.UUID("00000000-0000-0000-0000-0000000000c3")
|
||||
|
||||
sqla_table = SqlaTable(
|
||||
table_name="my_table",
|
||||
database=source_db,
|
||||
columns=[
|
||||
TableColumn(column_name="profit", type="INTEGER", uuid=column_uuid),
|
||||
],
|
||||
metrics=[
|
||||
SqlMetric(metric_name="cnt", expression="COUNT(*)", uuid=metric_uuid),
|
||||
],
|
||||
folders=[
|
||||
{
|
||||
"uuid": str(folder_uuid),
|
||||
"type": "folder",
|
||||
"name": "Custom",
|
||||
"children": [
|
||||
{"uuid": str(metric_uuid)},
|
||||
{"uuid": str(column_uuid)},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
db.session.add(sqla_table)
|
||||
db.session.flush()
|
||||
|
||||
# --- export (command-level YAML payload) ---
|
||||
config = yaml.safe_load(ExportDatasetsCommand._file_content(sqla_table)) # pylint: disable=protected-access
|
||||
|
||||
# The exported metric/column must carry their UUIDs so the folder
|
||||
# references survive the round trip.
|
||||
assert config["metrics"][0].get("uuid") == str(metric_uuid)
|
||||
assert any(col.get("uuid") == str(column_uuid) for col in config["columns"])
|
||||
|
||||
# The import schema must accept and preserve those UUIDs; without the schema
|
||||
# fields it would reject them as unknown and the round trip would break.
|
||||
loaded = ImportV1DatasetSchema().load(config)
|
||||
assert loaded["metrics"][0]["uuid"] == metric_uuid
|
||||
assert any(col["uuid"] == column_uuid for col in loaded["columns"])
|
||||
|
||||
# --- import into another workspace ---
|
||||
# Model a separate workspace: drop the source dataset so its UUIDs are free
|
||||
# (a fresh workspace has never seen them), then import against a fresh
|
||||
# database and a brand-new dataset UUID so the importer creates the
|
||||
# metric/column anew from the exported payload.
|
||||
db.session.delete(sqla_table)
|
||||
db.session.flush()
|
||||
|
||||
target_db = Database(database_name="target_db", sqlalchemy_uri="sqlite://")
|
||||
db.session.add(target_db)
|
||||
db.session.flush()
|
||||
config["database_id"] = target_db.id
|
||||
config["uuid"] = str(uuid.uuid4())
|
||||
|
||||
imported = import_dataset(config)
|
||||
|
||||
# The metric/column must be recreated with their original UUIDs so the
|
||||
# folder leaves still resolve to them — i.e. they stay in the custom folder.
|
||||
imported_metric = next(m for m in imported.metrics if m.metric_name == "cnt")
|
||||
assert imported_metric.uuid == metric_uuid
|
||||
imported_column = next(c for c in imported.columns if c.column_name == "profit")
|
||||
assert imported_column.uuid == column_uuid
|
||||
|
||||
# Every UUID referenced by a folder leaf must exist among the imported
|
||||
# metrics/columns; otherwise the item is orphaned back to the default folder.
|
||||
folder_leaf_uuids = {
|
||||
child["uuid"] for folder in imported.folders for child in folder["children"]
|
||||
}
|
||||
child_uuids = {str(m.uuid) for m in imported.metrics} | {
|
||||
str(c.uuid) for c in imported.columns
|
||||
}
|
||||
assert folder_leaf_uuids <= child_uuids
|
||||
|
||||
|
||||
def test_import_dataset_no_folder(mocker: MockerFixture, session: Session) -> None:
|
||||
"""
|
||||
Test importing a dataset that was exported without folders.
|
||||
|
||||
Reference in New Issue
Block a user