This is an automated email from the ASF dual-hosted git repository.
yongjiezhao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 7626c31372 feat: TreeMap migration (#20346)
7626c31372 is described below
commit 7626c3137234d76b065559913705e19c3f59cf7f
Author: Yongjie Zhao <[email protected]>
AuthorDate: Thu Jul 7 19:37:18 2022 +0800
feat: TreeMap migration (#20346)
---
...bea_add_advanced_data_types_to_column_models.py | 4 +-
..._22-04_c747c78868b6_migrating_legacy_treemap.py | 102 +++++++++++++++++
superset/utils/migrate_viz.py | 122 +++++++++++++++++++++
tests/unit_tests/conftest.py | 5 +-
.../utils/viz_migration/treemap_migration_test.py | 93 ++++++++++++++++
5 files changed, 324 insertions(+), 2 deletions(-)
diff --git
a/superset/migrations/versions/2021-05-27_16-10_6f139c533bea_add_advanced_data_types_to_column_models.py
b/superset/migrations/versions/2021-05-27_16-10_6f139c533bea_add_advanced_data_types_to_column_models.py
index bbbee47fad..114716c3da 100644
---
a/superset/migrations/versions/2021-05-27_16-10_6f139c533bea_add_advanced_data_types_to_column_models.py
+++
b/superset/migrations/versions/2021-05-27_16-10_6f139c533bea_add_advanced_data_types_to_column_models.py
@@ -14,10 +14,12 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-"""adding_advanced_data_type.py
+"""adding advanced data type to column models
+
Revision ID: 6f139c533bea
Revises: cbe71abde154
Create Date: 2021-05-27 16:10:59.567684
+
"""
import sqlalchemy as sa
diff --git
a/superset/migrations/versions/2022-06-30_22-04_c747c78868b6_migrating_legacy_treemap.py
b/superset/migrations/versions/2022-06-30_22-04_c747c78868b6_migrating_legacy_treemap.py
new file mode 100644
index 0000000000..c420af5fca
--- /dev/null
+++
b/superset/migrations/versions/2022-06-30_22-04_c747c78868b6_migrating_legacy_treemap.py
@@ -0,0 +1,102 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Migrating legacy TreeMap
+
+Revision ID: c747c78868b6
+Revises: e786798587de
+Create Date: 2022-06-30 22:04:17.686635
+
+"""
+
+# revision identifiers, used by Alembic.
+
+revision = "c747c78868b6"
+down_revision = "7fb8bca906d2"
+
+from alembic import op
+from sqlalchemy import and_, Column, Integer, String, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+from superset import db
+from superset.utils.migrate_viz import get_migrate_class, MigrateVizEnum
+
+treemap_processor = get_migrate_class[MigrateVizEnum.treemap]
+
+Base = declarative_base()
+
+
+class Slice(Base):
+ __tablename__ = "slices"
+
+ id = Column(Integer, primary_key=True)
+ slice_name = Column(String(250))
+ viz_type = Column(String(250))
+ params = Column(Text)
+ query_context = Column(Text)
+
+
+def upgrade():
+ bind = op.get_bind()
+ session = db.Session(bind=bind)
+
+ slices = session.query(Slice).filter(
+ Slice.viz_type == treemap_processor.source_viz_type
+ )
+ total = slices.count()
+ idx = 0
+ for slc in slices.yield_per(1000):
+ try:
+ idx += 1
+ print(f"Upgrading ({idx}/{total}): {slc.slice_name}#{slc.id}")
+ new_viz = treemap_processor.upgrade(slc)
+ session.merge(new_viz)
+ except Exception as exc:
+ print(
+ "Error while processing migration: '{}'\nError: {}\n".format(
+ slc.slice_name, str(exc)
+ )
+ )
+ session.commit()
+ session.close()
+
+
+def downgrade():
+ bind = op.get_bind()
+ session = db.Session(bind=bind)
+
+ slices = session.query(Slice).filter(
+ and_(
+ Slice.viz_type == treemap_processor.target_viz_type,
+ Slice.params.like("%form_data_bak%"),
+ )
+ )
+ total = slices.count()
+ idx = 0
+ for slc in slices.yield_per(1000):
+ try:
+ idx += 1
+ print(f"Downgrading ({idx}/{total}): {slc.slice_name}#{slc.id}")
+ new_viz = treemap_processor.downgrade(slc)
+ session.merge(new_viz)
+ except Exception as exc:
+ print(
+ "Error while processing migration: '{}'\nError: {}\n".format(
+ slc.slice_name, str(exc)
+ )
+ )
+ session.commit()
+ session.close()
diff --git a/superset/utils/migrate_viz.py b/superset/utils/migrate_viz.py
new file mode 100644
index 0000000000..65ae467cb8
--- /dev/null
+++ b/superset/utils/migrate_viz.py
@@ -0,0 +1,122 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import json
+from enum import Enum
+from typing import Dict, Set, Type, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from superset.models.slice import Slice
+
+
+# pylint: disable=invalid-name
+class MigrateVizEnum(str, Enum):
+ # the Enum member name is viz_type in database
+ treemap = "treemap"
+
+
+class MigrateViz:
+ remove_keys: Set[str] = set()
+ mapping_keys: Dict[str, str] = {}
+ source_viz_type: str
+ target_viz_type: str
+
+ def __init__(self, form_data: str) -> None:
+ self.data = json.loads(form_data)
+
+ def _pre_action(self) -> None:
+ """some actions before migrate"""
+
+ def _migrate(self) -> None:
+ if self.data.get("viz_type") != self.source_viz_type:
+ return
+
+ if "viz_type" in self.data:
+ self.data["viz_type"] = self.target_viz_type
+
+ rv_data = {}
+ for key, value in self.data.items():
+ if key in self.mapping_keys and self.mapping_keys[key] in rv_data:
+ raise ValueError("Duplicate key in target viz")
+
+ if key in self.mapping_keys:
+ rv_data[self.mapping_keys[key]] = value
+
+ if key in self.remove_keys:
+ continue
+
+ rv_data[key] = value
+
+ self.data = rv_data
+
+ def _post_action(self) -> None:
+ """some actions after migrate"""
+
+ @classmethod
+ def upgrade(cls, slc: Slice) -> Slice:
+ clz = cls(slc.params)
+ slc.viz_type = cls.target_viz_type
+ form_data_bak = clz.data.copy()
+
+ clz._pre_action()
+ clz._migrate()
+ clz._post_action()
+
+ # only backup params
+ slc.params = json.dumps({**clz.data, "form_data_bak": form_data_bak})
+
+ query_context = json.loads(slc.query_context or "{}")
+ if "form_data" in query_context:
+ query_context["form_data"] = clz.data
+ slc.query_context = json.dumps(query_context)
+ return slc
+
+ @classmethod
+ def downgrade(cls, slc: Slice) -> Slice:
+ form_data = json.loads(slc.params)
+ if "form_data_bak" in form_data and "viz_type" in form_data.get(
+ "form_data_bak"
+ ):
+ form_data_bak = form_data["form_data_bak"]
+ slc.params = json.dumps(form_data_bak)
+ slc.viz_type = form_data_bak.get("viz_type")
+
+ query_context = json.loads(slc.query_context or "{}")
+ if "form_data" in query_context:
+ query_context["form_data"] = form_data_bak
+ slc.query_context = json.dumps(query_context)
+ return slc
+
+
+class MigrateTreeMap(MigrateViz):
+ source_viz_type = "treemap"
+ target_viz_type = "treemap_v2"
+ remove_keys = {"metrics"}
+
+ def _pre_action(self) -> None:
+ if (
+ "metrics" in self.data
+ and isinstance(self.data["metrics"], list)
+ and len(self.data["metrics"]) > 0
+ ):
+ self.data["metric"] = self.data["metrics"][0]
+
+
+get_migrate_class: Dict[MigrateVizEnum, Type[MigrateViz]] = {
+ MigrateVizEnum.treemap: MigrateTreeMap,
+}
diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py
index 4560617d4b..1403e31249 100644
--- a/tests/unit_tests/conftest.py
+++ b/tests/unit_tests/conftest.py
@@ -17,6 +17,7 @@
# pylint: disable=redefined-outer-name, import-outside-toplevel
import importlib
+import os
from typing import Any, Callable, Iterator
import pytest
@@ -69,7 +70,9 @@ def app() -> Iterator[SupersetApp]:
app = SupersetApp(__name__)
app.config.from_object("superset.config")
- app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://"
+ app.config["SQLALCHEMY_DATABASE_URI"] = (
+ os.environ.get("SUPERSET__SQLALCHEMY_DATABASE_URI") or "sqlite://"
+ )
app.config["WTF_CSRF_ENABLED"] = False
app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = False
app.config["TESTING"] = True
diff --git a/tests/unit_tests/utils/viz_migration/treemap_migration_test.py
b/tests/unit_tests/utils/viz_migration/treemap_migration_test.py
new file mode 100644
index 0000000000..4bec5dec83
--- /dev/null
+++ b/tests/unit_tests/utils/viz_migration/treemap_migration_test.py
@@ -0,0 +1,93 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import json
+
+from superset.app import SupersetApp
+from superset.utils.migrate_viz import get_migrate_class, MigrateVizEnum
+
+treemap_form_data = """{
+ "adhoc_filters": [
+ {
+ "clause": "WHERE",
+ "comparator": [
+ "Edward"
+ ],
+ "expressionType": "SIMPLE",
+ "filterOptionName": "filter_xhbus6irfa_r10k9nwmwy",
+ "isExtra": false,
+ "isNew": false,
+ "operator": "IN",
+ "operatorId": "IN",
+ "sqlExpression": null,
+ "subject": "name"
+ }
+ ],
+ "color_scheme": "bnbColors",
+ "datasource": "2__table",
+ "extra_form_data": {},
+ "granularity_sqla": "ds",
+ "groupby": [
+ "state",
+ "gender"
+ ],
+ "metrics": [
+ "sum__num"
+ ],
+ "number_format": ",d",
+ "order_desc": true,
+ "row_limit": 10,
+ "time_range": "No filter",
+ "timeseries_limit_metric": "sum__num",
+ "treemap_ratio": 1.618033988749895,
+ "viz_type": "treemap"
+}
+"""
+
+treemap_processor = get_migrate_class[MigrateVizEnum.treemap]
+
+
+def test_treemap_migrate(app_context: SupersetApp) -> None:
+ from superset.models.slice import Slice
+
+ slc = Slice(
+ viz_type="treemap",
+ datasource_type="table",
+ params=treemap_form_data,
+ query_context=f'{{"form_data": {treemap_form_data}}}',
+ )
+
+ slc = treemap_processor.upgrade(slc)
+ assert slc.viz_type == treemap_processor.target_viz_type
+ # verify form_data
+ new_form_data = json.loads(slc.params)
+ assert new_form_data["metric"] == "sum__num"
+ assert new_form_data["viz_type"] == "treemap_v2"
+ assert "metrics" not in new_form_data
+ assert json.dumps(new_form_data["form_data_bak"], sort_keys=True) ==
json.dumps(
+ json.loads(treemap_form_data), sort_keys=True
+ )
+
+ # verify query_context
+ new_query_context = json.loads(slc.query_context)
+ assert new_query_context["form_data"]["viz_type"] == "treemap_v2"
+
+ # downgrade
+ slc = treemap_processor.downgrade(slc)
+ assert slc.viz_type == treemap_processor.source_viz_type
+ assert json.dumps(json.loads(slc.params), sort_keys=True) == json.dumps(
+ json.loads(treemap_form_data), sort_keys=True
+ )