diff --git a/superset/migrations/versions/58df9d617f14_add_on_saved_query_delete_tab_state_.py b/superset/migrations/versions/58df9d617f14_add_on_saved_query_delete_tab_state_.py new file mode 100644 index 0000000000000..220370f828049 --- /dev/null +++ b/superset/migrations/versions/58df9d617f14_add_on_saved_query_delete_tab_state_.py @@ -0,0 +1,66 @@ +# 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. +"""add_on_saved_query_delete_tab_state_null_constraint" + +Revision ID: 58df9d617f14 +Revises: 6766938c6065 +Create Date: 2022-03-16 23:24:40.278937 + +""" + +# revision identifiers, used by Alembic. +revision = "58df9d617f14" +down_revision = "6766938c6065" + +import sqlalchemy as sa +from alembic import op + +from superset.utils.core import generic_find_fk_constraint_name + + +def upgrade(): + bind = op.get_bind() + insp = sa.engine.reflection.Inspector.from_engine(bind) + + with op.batch_alter_table("tab_state") as batch_op: + batch_op.drop_constraint( + generic_find_fk_constraint_name("tab_state", {"id"}, "saved_query", insp), + type_="foreignkey", + ) + + batch_op.create_foreign_key( + "saved_query_id", + "saved_query", + ["saved_query_id"], + ["id"], + ondelete="SET NULL", + ) + + +def downgrade(): + bind = op.get_bind() + insp = sa.engine.reflection.Inspector.from_engine(bind) + + with op.batch_alter_table("tab_state") as batch_op: + batch_op.drop_constraint( + generic_find_fk_constraint_name("tab_state", {"id"}, "saved_query", insp), + type_="foreignkey", + ) + + batch_op.create_foreign_key( + "saved_query_id", "saved_query", ["saved_query_id"], ["id"], + ) diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py index d2e9b3fefb018..6a3b4ad8bfd7c 100644 --- a/superset/models/sql_lab.py +++ b/superset/models/sql_lab.py @@ -291,7 +291,9 @@ class TabState(Model, AuditMixinNullable, ExtraJSONMixin): hide_left_bar = Column(Boolean, default=False) # any saved queries that are associated with the Tab State - saved_query_id = Column(Integer, ForeignKey("saved_query.id"), nullable=True) + saved_query_id = Column( + Integer, ForeignKey("saved_query.id", ondelete="SET NULL"), nullable=True + ) saved_query = relationship("SavedQuery", foreign_keys=[saved_query_id]) def to_dict(self) -> Dict[str, Any]: diff --git a/superset/views/sql_lab.py b/superset/views/sql_lab.py index 5ec525b9cac73..49336a84a18d6 100644 --- a/superset/views/sql_lab.py +++ b/superset/views/sql_lab.py @@ -20,6 +20,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_appbuilder.security.decorators import has_access, has_access_api from flask_babel import lazy_gettext as _ +from sqlalchemy import and_ from superset import db, is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod @@ -228,6 +229,29 @@ def migrate_query( # pylint: disable=no-self-use def delete_query( # pylint: disable=no-self-use self, tab_state_id: int, client_id: str ) -> FlaskResponse: + # Before deleting the query, ensure it's not tied to any + # active tab as the last query. If so, replace the query + # with the latest one created in that tab + tab_state_query = db.session.query(TabState).filter_by( + id=tab_state_id, latest_query_id=client_id + ) + if tab_state_query.count(): + query = ( + db.session.query(Query) + .filter( + and_( + Query.client_id != client_id, + Query.user_id == g.user.get_id(), + Query.sql_editor_id == str(tab_state_id), + ), + ) + .order_by(Query.id.desc()) + .first() + ) + tab_state_query.update( + {"latest_query_id": query.client_id if query else None} + ) + db.session.query(Query).filter_by( client_id=client_id, user_id=g.user.get_id(),