Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""add position column to collection associations for ordering

Revision ID: e1f2a3b4c5d6
Revises: dcf8572d3a17
Create Date: 2026-02-13 10:00:00.000000

"""

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision = "e1f2a3b4c5d6"
down_revision = "dcf8572d3a17"
branch_labels = None
depends_on = None


def upgrade():
"""
Add position column to collection association tables to support user-controlled ordering.
Backfills existing rows with sequential positions ordered by the foreign key ID.
"""
# 1. Add column with a temporary server default so existing rows are valid.
op.add_column("collection_score_sets", sa.Column("position", sa.Integer(), nullable=False, server_default="0"))
op.add_column("collection_experiments", sa.Column("position", sa.Integer(), nullable=False, server_default="0"))

# 2. Backfill: assign sequential positions within each collection, ordered by score_set_id / experiment_id ASC.
# This gives existing rows a deterministic order rather than all sharing position 0.
conn = op.get_bind()
conn.execute(
sa.text(
"""
UPDATE collection_score_sets AS css
SET position = sub.rn
FROM (
SELECT collection_id, score_set_id,
ROW_NUMBER() OVER (PARTITION BY collection_id ORDER BY score_set_id) - 1 AS rn
FROM collection_score_sets
) AS sub
WHERE css.collection_id = sub.collection_id AND css.score_set_id = sub.score_set_id
"""
)
)
conn.execute(
sa.text(
"""
UPDATE collection_experiments AS ce
SET position = sub.rn
FROM (
SELECT collection_id, experiment_id,
ROW_NUMBER() OVER (PARTITION BY collection_id ORDER BY experiment_id) - 1 AS rn
FROM collection_experiments
) AS sub
WHERE ce.collection_id = sub.collection_id AND ce.experiment_id = sub.experiment_id
"""
)
)

# 3. Remove server defaults — the ORM model's `default=0` handles new inserts.
op.alter_column("collection_score_sets", "position", server_default=None)
op.alter_column("collection_experiments", "position", server_default=None)


def downgrade():
"""Remove position columns from collection association tables."""
op.drop_column("collection_experiments", "position")
op.drop_column("collection_score_sets", "position")
3,472 changes: 1,752 additions & 1,720 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["poetry-core"]
requires = ["setuptools", "poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
Expand Down Expand Up @@ -33,7 +33,7 @@ eutils = "~0.6.0"
email_validator = "~2.1.1"
numpy = "~1.26"
httpx = "~0.26.0"
pandas = "~1.4.1"
pandas = ">=2.2.0,<3.0.0"
pydantic = "~2.10.0"
python-dotenv = "~0.20.0"
python-json-logger = "~2.0.7"
Expand Down
21 changes: 13 additions & 8 deletions src/mavedb/lib/validation/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,36 @@
"""

from typing import Optional, Sequence, Union
from typing_extensions import TypedDict

from pydantic import TypeAdapter
from typing_extensions import TypedDict

from mavedb.models.enums.score_calibration_relation import ScoreCalibrationRelation
from mavedb.models.enums.contribution_role import ContributionRole
from mavedb.models.experiment_set import ExperimentSet
from mavedb.models.collection_user_association import CollectionUserAssociation
from mavedb.models.enums.contribution_role import ContributionRole
from mavedb.models.enums.score_calibration_relation import ScoreCalibrationRelation
from mavedb.models.experiment import Experiment
from mavedb.models.experiment_publication_identifier import ExperimentPublicationIdentifierAssociation
from mavedb.models.score_set_publication_identifier import ScoreSetPublicationIdentifierAssociation
from mavedb.models.experiment_set import ExperimentSet
from mavedb.models.score_calibration_publication_identifier import ScoreCalibrationPublicationIdentifierAssociation
from mavedb.models.experiment import Experiment
from mavedb.models.score_set import ScoreSet
from mavedb.models.score_set_publication_identifier import ScoreSetPublicationIdentifierAssociation
from mavedb.models.target_gene import TargetGene
from mavedb.models.user import User
from mavedb.view_models.external_gene_identifier_offset import ExternalGeneIdentifierOffset
from mavedb.view_models.publication_identifier import PublicationIdentifier


# TODO(#372)
def transform_score_set_list_to_urn_list(score_sets: Optional[list[ScoreSet]]) -> list[Optional[str]]:
def transform_score_set_list_to_urn_list(
score_sets: Optional[list[ScoreSet]], include_superseding: bool = False
) -> list[Optional[str]]:
if not score_sets:
return []

return [score_set.urn for score_set in score_sets if score_set.superseding_score_set is None]
if include_superseding:
return [score_set.urn for score_set in score_sets]
else:
return [score_set.urn for score_set in score_sets if score_set.superseding_score_set is None]


def transform_experiment_list_to_urn_list(experiments: Optional[list[Experiment]]) -> list[Optional[str]]:
Expand Down
4 changes: 2 additions & 2 deletions src/mavedb/logging/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import os
from importlib.resources import files

import yaml
from pkg_resources import resource_stream


def load_stock_config(name="default"):
"""
Loads a built-in stock logging configuration based on *name*.
"""
with resource_stream(__package__, f"configurations/{name}.yaml") as file:
with files(__package__).joinpath(f"configurations/{name}.yaml").open("r") as file:
return load_config(file)


Expand Down
35 changes: 23 additions & 12 deletions src/mavedb/models/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
import mavedb.models.collection_user_association
from mavedb.db.base import Base
from mavedb.lib.urns import generate_collection_urn
from mavedb.models.collection_association import (
collection_experiments_association_table,
collection_score_sets_association_table,
)
from mavedb.models.collection_experiment_association import CollectionExperimentAssociation
from mavedb.models.collection_score_set_association import CollectionScoreSetAssociation

from .experiment import Experiment
from .score_set import ScoreSet
Expand Down Expand Up @@ -55,13 +53,26 @@ class Collection(Base):
),
)

experiments: Mapped[list[Experiment]] = relationship(
"Experiment",
secondary=collection_experiments_association_table,
back_populates="collections",
# Ordered association relationships
score_set_associations: Mapped[list[CollectionScoreSetAssociation]] = relationship(
"CollectionScoreSetAssociation",
back_populates="collection",
cascade="all, delete-orphan",
order_by="CollectionScoreSetAssociation.position",
)
experiment_associations: Mapped[list[CollectionExperimentAssociation]] = relationship(
"CollectionExperimentAssociation",
back_populates="collection",
cascade="all, delete-orphan",
order_by="CollectionExperimentAssociation.position",
)

# Convenient proxies for direct access to ordered score sets and experiments
experiments: AssociationProxy[list[Experiment]] = association_proxy(
"experiment_associations",
"experiment",
)
score_sets: Mapped[list[ScoreSet]] = relationship(
"ScoreSet",
secondary=collection_score_sets_association_table,
back_populates="collections",
score_sets: AssociationProxy[list[ScoreSet]] = association_proxy(
"score_set_associations",
"score_set",
)
18 changes: 0 additions & 18 deletions src/mavedb/models/collection_association.py

This file was deleted.

36 changes: 36 additions & 0 deletions src/mavedb/models/collection_experiment_association.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Association model for Collection-Experiment many-to-many relationship with ordering.
"""

from typing import TYPE_CHECKING

from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.orm import Mapped, relationship

from mavedb.db.base import Base

if TYPE_CHECKING:
from mavedb.models.collection import Collection
from mavedb.models.experiment import Experiment


class CollectionExperimentAssociation(Base):
"""
Association model for the ordered many-to-many relationship between Collections and Experiments.

The position column maintains the user-specified ordering of experiments within each collection.
"""

__tablename__ = "collection_experiments"

collection_id = Column(Integer, ForeignKey("collections.id"), primary_key=True)
experiment_id = Column(Integer, ForeignKey("experiments.id"), primary_key=True)
position = Column(Integer, nullable=False, default=0)

collection: Mapped["Collection"] = relationship(
"mavedb.models.collection.Collection",
back_populates="experiment_associations",
)
experiment: Mapped["Experiment"] = relationship(
"mavedb.models.experiment.Experiment",
)
36 changes: 36 additions & 0 deletions src/mavedb/models/collection_score_set_association.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Association model for Collection-ScoreSet many-to-many relationship with ordering.
"""

from typing import TYPE_CHECKING

from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.orm import Mapped, relationship

from mavedb.db.base import Base

if TYPE_CHECKING:
from mavedb.models.collection import Collection
from mavedb.models.score_set import ScoreSet


class CollectionScoreSetAssociation(Base):
"""
Association model for the ordered many-to-many relationship between Collections and ScoreSets.

The position column maintains the user-specified ordering of score sets within each collection.
"""

__tablename__ = "collection_score_sets"

collection_id = Column(Integer, ForeignKey("collections.id"), primary_key=True)
score_set_id = Column(Integer, ForeignKey("scoresets.id"), primary_key=True)
position = Column(Integer, nullable=False, default=0)

collection: Mapped["Collection"] = relationship(
"mavedb.models.collection.Collection",
back_populates="score_set_associations",
)
score_set: Mapped["ScoreSet"] = relationship(
"mavedb.models.score_set.ScoreSet",
)
12 changes: 5 additions & 7 deletions src/mavedb/models/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@

from mavedb.db.base import Base
from mavedb.lib.temp_urns import generate_temp_urn
from mavedb.models.collection_association import (
collection_experiments_association_table,
)
from mavedb.models.contributor import Contributor
from mavedb.models.controlled_keyword import ControlledKeyword
from mavedb.models.doi_identifier import DoiIdentifier
Expand Down Expand Up @@ -84,16 +81,17 @@ class Experiment(Base):
num_score_sets = Column("num_scoresets", Integer, nullable=False, default=0)
score_sets: Mapped[List["ScoreSet"]] = relationship(back_populates="experiment", cascade="all, delete-orphan")

# View-only back-references to collections (actual relationship managed on Collection side)
collections: Mapped[list["Collection"]] = relationship(
"Collection",
secondary=collection_experiments_association_table,
back_populates="experiments",
secondary="collection_experiments",
viewonly=True,
)
official_collections: Mapped[list["Collection"]] = relationship(
"Collection",
secondary=collection_experiments_association_table,
secondary="collection_experiments",
primaryjoin="Experiment.id == collection_experiments.c.experiment_id",
secondaryjoin="and_(collection_experiments.c.collection_id == Collection.id, Collection.badge_name != None)",
back_populates="experiments",
viewonly=True,
)

Expand Down
12 changes: 6 additions & 6 deletions src/mavedb/models/score_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import mavedb.models.score_set_publication_identifier
from mavedb.db.base import Base
from mavedb.models.collection_association import collection_score_sets_association_table
from mavedb.models.contributor import Contributor
from mavedb.models.doi_identifier import DoiIdentifier
from mavedb.models.enums.mapping_state import MappingState
Expand All @@ -22,9 +21,9 @@

if TYPE_CHECKING:
from mavedb.models.collection import Collection
from mavedb.models.score_calibration import ScoreCalibration
from mavedb.models.target_gene import TargetGene
from mavedb.models.variant import Variant
from mavedb.models.score_calibration import ScoreCalibration

# from .raw_read_identifier import SraIdentifier
from mavedb.lib.temp_urns import generate_temp_urn
Expand Down Expand Up @@ -188,16 +187,17 @@ class ScoreSet(Base):
"ScoreCalibration", back_populates="score_set", cascade="all, delete-orphan"
)

# View-only back-references to collections (actual relationship managed on Collection side)
collections: Mapped[list["Collection"]] = relationship(
"Collection",
secondary=collection_score_sets_association_table,
back_populates="score_sets",
secondary="collection_score_sets",
viewonly=True,
)
official_collections: Mapped[list["Collection"]] = relationship(
"Collection",
secondary=collection_score_sets_association_table,
secondary="collection_score_sets",
primaryjoin="ScoreSet.id == collection_score_sets.c.score_set_id",
secondaryjoin="and_(collection_score_sets.c.collection_id == Collection.id, Collection.badge_name != None)",
back_populates="score_sets",
viewonly=True,
)

Expand Down
Loading