diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/06596ce2bc3e_add_order_to_function_job_collections.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/06596ce2bc3e_add_order_to_function_job_collections.py new file mode 100644 index 00000000000..dbe50c5d9d9 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/06596ce2bc3e_add_order_to_function_job_collections.py @@ -0,0 +1,73 @@ +"""add order to function job collections + +Revision ID: 06596ce2bc3e +Revises: 9dddb16914a4 +Create Date: 2025-10-08 13:54:39.943703+00:00 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "06596ce2bc3e" +down_revision = "9dddb16914a4" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "funcapi_function_job_collections_to_function_jobs", + sa.Column("order", sa.Integer(), default=0, nullable=True), + ) + # Set the "order" column so that it restarts from 1 for each collection_id, + # ordering by function_job_id within each collection + op.execute( + sa.text( + """ + UPDATE funcapi_function_job_collections_to_function_jobs + SET "order" = sub.row_number + FROM ( + SELECT function_job_collection_uuid, function_job_uuid, + ROW_NUMBER() OVER (PARTITION BY function_job_collection_uuid) AS row_number + FROM funcapi_function_job_collections_to_function_jobs + ) AS sub + WHERE funcapi_function_job_collections_to_function_jobs.function_job_collection_uuid = sub.function_job_collection_uuid + AND funcapi_function_job_collections_to_function_jobs.function_job_uuid = sub.function_job_uuid + """ + ) + ) + op.drop_constraint( + "funcapi_function_job_collections_to_function_jobs_pk", + "funcapi_function_job_collections_to_function_jobs", + type_="primary", + ) + op.create_primary_key( + "funcapi_function_job_collections_to_function_jobs_pk", + "funcapi_function_job_collections_to_function_jobs", + ["function_job_collection_uuid", "order"], + ) + op.alter_column( + "funcapi_function_job_collections_to_function_jobs", + "order", + existing_type=sa.Integer(), + nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + op.drop_constraint( + "funcapi_function_job_collections_to_function_jobs_pk", + "funcapi_function_job_collections_to_function_jobs", + type_="primary", + ) + op.create_primary_key( + "funcapi_function_job_collections_to_function_jobs_pk", + "funcapi_function_job_collections_to_function_jobs", + ["function_job_collection_uuid", "function_job_uuid"], + ) + + op.drop_column("funcapi_function_job_collections_to_function_jobs", "order") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/f641b3eacafd_merge_06596ce2bc3e_a6289977e057.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/f641b3eacafd_merge_06596ce2bc3e_a6289977e057.py new file mode 100644 index 00000000000..e0ca536357c --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/f641b3eacafd_merge_06596ce2bc3e_a6289977e057.py @@ -0,0 +1,21 @@ +"""merge 06596ce2bc3e a6289977e057 + +Revision ID: f641b3eacafd +Revises: 06596ce2bc3e, a6289977e057 +Create Date: 2025-10-14 07:10:39.664119+00:00 + +""" + +# revision identifiers, used by Alembic. +revision = "f641b3eacafd" +down_revision = ("06596ce2bc3e", "a6289977e057") +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_to_function_jobs_table.py b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_to_function_jobs_table.py index ed8ec97249e..d96443f27c0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_to_function_jobs_table.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_to_function_jobs_table.py @@ -35,11 +35,18 @@ nullable=False, doc="Unique identifier of the function job", ), + sa.Column( + "order", + sa.Integer, + nullable=False, + doc="Order of the function job in the collection (1-based)", + ), column_created_datetime(), column_modified_datetime(), sa.PrimaryKeyConstraint( "function_job_collection_uuid", "function_job_uuid", + "order", name="funcapi_function_job_collections_to_function_jobs_pk", ), ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_function_job_collections_repository.py b/services/web/server/src/simcore_service_webserver/functions/_function_job_collections_repository.py index 154efcc15bd..3a0dd91642d 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_function_job_collections_repository.py +++ b/services/web/server/src/simcore_service_webserver/functions/_function_job_collections_repository.py @@ -95,12 +95,13 @@ async def create_function_job_collection( row ) job_collection_entries: list[Row] = [] - for job_id in job_ids: + for order, job_id in enumerate(job_ids, 1): result = await transaction.execute( function_job_collections_to_function_jobs_table.insert() .values( function_job_collection_uuid=function_job_collection_db.uuid, function_job_uuid=job_id, + order=order, ) .returning( function_job_collections_to_function_jobs_table.c.function_job_collection_uuid, @@ -228,10 +229,15 @@ async def list_function_job_collections( job_ids = [ job_row.function_job_uuid async for job_row in await conn.stream( - function_job_collections_to_function_jobs_table.select().where( + function_job_collections_to_function_jobs_table.select() + .where( function_job_collections_to_function_jobs_table.c.function_job_collection_uuid == row.uuid ) + .order_by( + function_job_collections_to_function_jobs_table.c.order, + function_job_collections_to_function_jobs_table.c.function_job_uuid, + ) ) ] collections.append((collection, job_ids)) @@ -278,10 +284,15 @@ async def get_function_job_collection( job_ids = [ job_row.function_job_uuid async for job_row in await conn.stream( - function_job_collections_to_function_jobs_table.select().where( + function_job_collections_to_function_jobs_table.select() + .where( function_job_collections_to_function_jobs_table.c.function_job_collection_uuid == row.uuid ) + .order_by( + function_job_collections_to_function_jobs_table.c.order, + function_job_collections_to_function_jobs_table.c.function_job_uuid, + ) ) ] diff --git a/services/web/server/src/simcore_service_webserver/functions/_function_jobs_repository.py b/services/web/server/src/simcore_service_webserver/functions/_function_jobs_repository.py index 3104e62c7a0..7aeff55cba4 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_function_jobs_repository.py +++ b/services/web/server/src/simcore_service_webserver/functions/_function_jobs_repository.py @@ -209,6 +209,10 @@ async def list_function_jobs_with_status( function_job_collections_to_function_jobs_table.c.function_job_collection_uuid == filter_by_function_job_collection_id ) + .order_by( + function_job_collections_to_function_jobs_table.c.order, + function_job_collections_to_function_jobs_table.c.function_job_uuid, + ) ) filter_conditions = sqlalchemy.and_( filter_conditions, diff --git a/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_job_collections_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_job_collections_controller_rpc.py index 47ca7d4d37f..32e04bc8936 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_job_collections_controller_rpc.py +++ b/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_job_collections_controller_rpc.py @@ -159,6 +159,69 @@ async def test_function_job_collection( ) +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_create_function_job_collection_same_function_job_uuid( + client: TestClient, + add_user_function_api_access_rights: None, + create_fake_function_obj: Callable[[FunctionClass], Function], + webserver_rpc_client: WebServerRpcClient, + logged_user: UserInfoDict, + other_logged_user: UserInfoDict, + user_without_function_api_access_rights: UserInfoDict, + osparc_product_name: ProductName, +): + # Register the function first + registered_function = await webserver_rpc_client.functions.register_function( + function=create_fake_function_obj(FunctionClass.PROJECT), + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_function.uid is not None + + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + job_creation_task_id=None, + ) + # Register the function job + function_job_ids = [] + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + job_creation_task_id=None, + ) + # Register the function job + registered_job = await webserver_rpc_client.functions.register_function_job( + function_job=registered_function_job, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_job.uid is not None + + function_job_ids = [registered_job.uid] * 3 + + function_job_collection = FunctionJobCollection( + title="Test Function Job Collection", + description="A test function job collection", + job_ids=function_job_ids, + ) + + assert function_job_collection.job_ids[0] == registered_job.uid + assert function_job_collection.job_ids[1] == registered_job.uid + assert function_job_collection.job_ids[2] == registered_job.uid + + @pytest.mark.parametrize( "user_role", [UserRole.USER], diff --git a/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_jobs_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_jobs_controller_rpc.py index ef534919388..0a22743775b 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_jobs_controller_rpc.py +++ b/services/web/server/tests/unit/with_dbs/04/functions/wb-api-server/test_function_jobs_controller_rpc.py @@ -325,15 +325,14 @@ async def test_list_function_jobs_filtering( ) ) + job_ids = [ + job.uid + for job in first_registered_function_jobs[1:2] + + second_registered_function_jobs[0:1] + ] function_job_collection = ( await webserver_rpc_client.functions.register_function_job_collection( - function_job_collection=FunctionJobCollection( - job_ids=[ - job.uid - for job in first_registered_function_jobs[1:2] - + second_registered_function_jobs[0:1] - ] - ), + function_job_collection=FunctionJobCollection(job_ids=job_ids), user_id=logged_user["id"], product_name=osparc_product_name, ) @@ -381,6 +380,7 @@ async def test_list_function_jobs_filtering( # Assert the list contains the registered job assert len(jobs) == 2 + assert [job.uid for job in jobs] == job_ids assert jobs[0].uid == first_registered_function_jobs[1].uid assert jobs[1].uid == second_registered_function_jobs[0].uid