From 2ed36c992f2aa49376c6c686be82e4615d110e3d Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 17 Dec 2024 08:18:08 +0100 Subject: [PATCH] Fix importing shared workflows with deeply nested subworkflows We need to pass the user also to the workflow copy method when copying subworkflow steps. Adds a few bonus type annotations that made it easier to debug. Fixes https://sentry.galaxyproject.org/share/issue/454be6bbb98b43a1aa504309e1c6bc7b/:: ``` NotNullViolation: null value in column "user_id" of relation "stored_workflow" violates not-null constraint DETAIL: Failing row contains (296863, 2024-12-16 17:04:34.329066, 2024-12-16 17:04:34.329067, null, null, TreeValGal bed to bigwig, f, f, null, f, null, t). File "sqlalchemy/engine/base.py", line 2116, in _exec_insertmany_context dialect.do_execute( File "sqlalchemy/engine/default.py", line 924, in do_execute cursor.execute(statement, parameters) IntegrityError: (psycopg2.errors.NotNullViolation) null value in column "user_id" of relation "stored_workflow" violates not-null constraint DETAIL: Failing row contains (296863, 2024-12-16 17:04:34.329066, 2024-12-16 17:04:34.329067, null, null, TreeValGal bed to bigwig, f, f, null, f, null, t). [SQL: INSERT INTO stored_workflow (create_time, update_time, user_id, latest_workflow_id, name, deleted, hidden, importable, slug, from_path, published) SELECT p0::TIMESTAMP WITHOUT TIME ZONE, p1::TIMESTAMP WITHOUT TIME ZONE, p2::INTEGER, p3::INTEGER, p4:: ... 1138 characters truncated ... p9, p10, sen_counter) ORDER BY sen_counter RETURNING stored_workflow.id, stored_workflow.id AS id__1] [parameters: {'from_path__0': None, 'user_id__0': 91803, 'name__0': 'TreeValGalBaseOneHaplotype', 'create_time__0': datetime.datetime(2024, 12, 16, 17, 4, 34, 329062), 'update_time__0': datetime.datetime(2024, 12, 16, 17, 4, 34, 329065), 'deleted__0': False, 'published__0': False, 'importable__0': False, 'latest_workflow_id__0': None, 'hidden... File "galaxy/web/framework/middleware/error.py", line 167, in __call__ app_iter = self.application(environ, sr_checker) File "/cvmfs/main.galaxyproject.org/venv/lib/python3.11/site-packages/paste/httpexceptions.py", line 635, in __call__ return self.application(environ, start_response) File "galaxy/web/framework/base.py", line 176, in __call__ return self.handle_request(request_id, path_info, environ, start_response) File "galaxy/web/framework/base.py", line 271, in handle_request body = method(trans, **kwargs) File "galaxy/web/framework/decorators.py", line 94, in decorator return func(self, trans, *args, **kwargs) File "galaxy/webapps/galaxy/controllers/workflow.py", line 125, in imp self._import_shared_workflow(trans, stored) File "galaxy/webapps/base/controller.py", line 1161, in _import_shared_workflow session.commit() File "sqlalchemy/orm/scoping.py", line 597, in commit return self._proxied.commit() File "sqlalchemy/orm/session.py", line 2017, in commit trans.commit(_to_root=True) File "", line 2, in commit # Copyright (C) 2005-2024 the SQLAlchemy authors and contributors File "sqlalchemy/orm/state_changes.py", line 139, in _go ret_value = fn(self, *arg, **kw) File "sqlalchemy/orm/session.py", line 1302, in commit self._prepare_impl() File "", line 2, in _prepare_impl # Copyright (C) 2005-2024 the SQLAlchemy authors and contributors File "sqlalchemy/orm/state_changes.py", line 139, in _go ret_value = fn(self, *arg, **kw) File "sqlalchemy/orm/session.py", line 1277, in _prepare_impl self.session.flush() File "sqlalchemy/orm/session.py", line 4341, in flush self._flush(objects) File "sqlalchemy/orm/session.py", line 4476, in _flush with util.safe_reraise(): File "sqlalchemy/util/langhelpers.py", line 146, in __exit__ raise exc_value.with_traceback(exc_tb) File "sqlalchemy/orm/session.py", line 4437, in _flush flush_context.execute() File "sqlalchemy/orm/unitofwork.py", line 466, in execute rec.execute(self) File "sqlalchemy/orm/unitofwork.py", line 642, in execute util.preloaded.orm_persistence.save_obj( File "sqlalchemy/orm/persistence.py", line 93, in save_obj _emit_insert_statements( File "sqlalchemy/orm/persistence.py", line 1143, in _emit_insert_statements result = connection.execute( File "sqlalchemy/engine/base.py", line 1418, in execute return meth( File "sqlalchemy/sql/elements.py", line 515, in _execute_on_connection return connection._execute_clauseelement( File "sqlalchemy/engine/base.py", line 1640, in _execute_clauseelement ret = self._execute_context( File "sqlalchemy/engine/base.py", line 1844, in _execute_context return self._exec_insertmany_context(dialect, context) File "sqlalchemy/engine/base.py", line 2124, in _exec_insertmany_context self._handle_dbapi_exception( File "sqlalchemy/engine/base.py", line 2353, in _handle_dbapi_exception raise sqlalchemy_exception.with_traceback(exc_info[2]) from e File "sqlalchemy/engine/base.py", line 2116, in _exec_insertmany_context dialect.do_execute( File "sqlalchemy/engine/default.py", line 924, in do_execute cursor.execute(statement, parameters) ``` --- lib/galaxy/model/__init__.py | 8 ++++---- lib/galaxy/webapps/base/controller.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 9843696f0db8..1dd75cee6309 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -7616,7 +7616,7 @@ class StoredWorkflow(Base, HasTags, Dictifiable, RepresentById, UsesCreateAndUpd order_by=lambda: -Workflow.id, cascade_backrefs=False, ) - latest_workflow = relationship( + latest_workflow: Mapped["Workflow"] = relationship( "Workflow", post_update=True, primaryjoin=(lambda: StoredWorkflow.latest_workflow_id == Workflow.id), @@ -7722,7 +7722,7 @@ def show_in_tool_panel(self, user_id): ) return bool(sa_session.scalar(stmt)) - def copy_tags_from(self, target_user, source_workflow): + def copy_tags_from(self, target_user, source_workflow: "StoredWorkflow"): # Override to only copy owner tags. for src_swta in source_workflow.owner_tags: new_swta = src_swta.copy() @@ -8239,9 +8239,9 @@ def copy_to(self, copied_step, step_mapping, user=None): copied_subworkflow = subworkflow else: # Can this even happen, building a workflow with a subworkflow you don't own ? - copied_subworkflow = subworkflow.copy() + copied_subworkflow = subworkflow.copy(user=user) stored_workflow = StoredWorkflow( - user, name=copied_subworkflow.name, workflow=copied_subworkflow, hidden=True + user=user, name=copied_subworkflow.name, workflow=copied_subworkflow, hidden=True ) copied_subworkflow.stored_workflow = stored_workflow copied_step.subworkflow = copied_subworkflow diff --git a/lib/galaxy/webapps/base/controller.py b/lib/galaxy/webapps/base/controller.py index 83c3ae620560..4f5d85de86d1 100644 --- a/lib/galaxy/webapps/base/controller.py +++ b/lib/galaxy/webapps/base/controller.py @@ -1144,7 +1144,7 @@ def get_stored_workflow_steps(self, trans, stored_workflow: model.StoredWorkflow except exceptions.ToolMissingException: pass - def _import_shared_workflow(self, trans, stored): + def _import_shared_workflow(self, trans, stored: model.StoredWorkflow): """Imports a shared workflow""" # Copy workflow. imported_stored = model.StoredWorkflow()