Skip to content

Commit fa7b8ef

Browse files
Unify activity status (#513)
1 parent f56baec commit fa7b8ef

26 files changed

+292
-126
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
"""unify_activity_status
2+
3+
Revision ID: 447c8883c88f
4+
Revises: 07064e01c345
5+
Create Date: 2026-01-20 17:59:31.059678
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
from sqlalchemy.dialects import postgresql
14+
15+
from sqlalchemy import Text, text, inspect
16+
import app.db.types
17+
from app.db.model import Activity
18+
19+
# revision identifiers, used by Alembic.
20+
revision: str = "447c8883c88f"
21+
down_revision: Union[str, None] = "07064e01c345"
22+
branch_labels: Union[str, Sequence[str], None] = None
23+
depends_on: Union[str, Sequence[str], None] = None
24+
25+
26+
TABLES_WITHOUT_STATUS = [
27+
"analysis_notebook_execution",
28+
"calibration",
29+
"circuit_extraction_config_generation",
30+
"ion_channel_modeling_config_generation",
31+
"simulation_generation",
32+
"skeletonization_config_generation",
33+
"validation",
34+
]
35+
36+
# move status from these tables to the activity parent table
37+
TABLES_TO_MOVE_STATUS = [
38+
{
39+
"table": "circuit_extraction_execution",
40+
"column": "status",
41+
"enum": {
42+
"name": "circuit_extraction_execution_status",
43+
"values": ["created", "pending", "running", "done", "error"],
44+
},
45+
},
46+
{
47+
"table": "skeletonization_execution",
48+
"column": "status",
49+
"enum": {
50+
"name": "skeletonizationexecutionstatus",
51+
"values": ["created", "pending", "running", "done", "error"],
52+
},
53+
},
54+
{
55+
"table": "ion_channel_modeling_execution",
56+
"column": "status",
57+
"enum": {
58+
"name": "ion_channel_modeling_execution_status",
59+
"values": ["created", "pending", "running", "done", "error"],
60+
},
61+
},
62+
{
63+
"table": "simulation_execution",
64+
"column": "status",
65+
"enum": {
66+
"name": "simulation_execution_status",
67+
"values": ["created", "pending", "running", "done", "error", "cancelled"],
68+
},
69+
},
70+
]
71+
72+
73+
# remap table status to activity status without moving it
74+
TABLES_TO_DROP_STATUS = [
75+
{
76+
"table": "single_neuron_simulation",
77+
"column": "status",
78+
"enum": {
79+
"name": "singleneuronsimulationstatus",
80+
"values": ["started", "failure", "success"],
81+
"default": "success",
82+
},
83+
},
84+
{
85+
"table": "single_neuron_synaptome_simulation",
86+
"column": "status",
87+
"enum": {
88+
"name": "singleneuronsimulationstatus",
89+
"values": ["started", "failure", "success"],
90+
"default": "success",
91+
},
92+
},
93+
]
94+
95+
96+
def _activity_enum():
97+
return sa.Enum(
98+
"created",
99+
"pending",
100+
"running",
101+
"done",
102+
"error",
103+
"cancelled",
104+
name="activitystatus",
105+
)
106+
107+
108+
def _create_activity_status_column(op):
109+
"""Create activity status collumn and fill it with done."""
110+
activity_enum = _activity_enum()
111+
activity_enum.create(op.get_bind())
112+
op.add_column(
113+
"activity",
114+
sa.Column("status", activity_enum, server_default=sa.text("'done'"), nullable=False),
115+
)
116+
return activity_enum
117+
118+
119+
def _using_expr(col: str, mapping: dict[str, str]) -> str:
120+
if not mapping:
121+
return f"{col}::text::activitystatus"
122+
123+
cases = "\n".join(f"WHEN '{old}' THEN '{new}'" for old, new in mapping.items())
124+
125+
return f"""
126+
(
127+
CASE {col}::text
128+
{cases}
129+
ELSE {col}::text
130+
END
131+
)::activitystatus
132+
"""
133+
134+
135+
def _move_table_statuses(op):
136+
def _move_status(op, from_table: str, from_column: str, to_table: str, mapping):
137+
cases = _using_expr(f"{from_table}.{from_column}", mapping)
138+
op.execute(f"""
139+
UPDATE {to_table}
140+
SET status = {cases}
141+
FROM {from_table}
142+
WHERE {to_table}.id = {from_table}.id
143+
""")
144+
op.drop_column(from_table, "status")
145+
146+
for t in TABLES_TO_MOVE_STATUS:
147+
_move_status(op, t["table"], t["column"], "activity", {})
148+
postgresql.ENUM(*t["enum"]["values"], name=t["enum"]["name"]).drop(op.get_bind())
149+
150+
151+
def _drop_table_statuses(op):
152+
for t in TABLES_TO_DROP_STATUS:
153+
op.drop_column(t["table"], t["column"])
154+
for t in TABLES_TO_DROP_STATUS:
155+
enum = postgresql.ENUM(*t["enum"]["values"], name=t["enum"]["name"])
156+
enum.drop(op.get_bind(), checkfirst=True)
157+
158+
159+
def upgrade() -> None:
160+
activity_enum = _create_activity_status_column(op)
161+
_move_table_statuses(op)
162+
_drop_table_statuses(op)
163+
164+
conn = op.get_bind()
165+
166+
# for the activity tables that did not have a status a "done" default
167+
# should be set in the acticity table
168+
for table in TABLES_WITHOUT_STATUS:
169+
count = conn.execute(
170+
text(f"""
171+
SELECT COUNT(*)
172+
FROM {table} t
173+
JOIN activity a ON a.id = t.id
174+
WHERE a.status != 'done'
175+
""")
176+
).scalar()
177+
178+
if count:
179+
raise RuntimeError(f"{table}: {count} rows have activity.status != 'done'")
180+
181+
182+
def _create_table_enums(conn):
183+
"""Create all old enums."""
184+
enums = {}
185+
for t in TABLES_TO_MOVE_STATUS:
186+
table_enum = postgresql.ENUM(*t["enum"]["values"], name=t["enum"]["name"])
187+
table_enum.create(conn, checkfirst=True)
188+
enums[t["enum"]["name"]] = table_enum
189+
for t in TABLES_TO_DROP_STATUS:
190+
table_enum = postgresql.ENUM(*t["enum"]["values"], name=t["enum"]["name"])
191+
table_enum.create(conn, checkfirst=True)
192+
enums[t["enum"]["name"]] = table_enum
193+
return enums
194+
195+
196+
def _create_table_columns(enums):
197+
for t in TABLES_TO_MOVE_STATUS:
198+
op.add_column(
199+
t["table"],
200+
sa.Column(t["column"], enums[t["enum"]["name"]], nullable=True),
201+
)
202+
for t in TABLES_TO_DROP_STATUS:
203+
op.add_column(
204+
t["table"],
205+
sa.Column(
206+
t["column"],
207+
enums[t["enum"]["name"]],
208+
nullable=False,
209+
server_default=t["enum"]["default"],
210+
),
211+
)
212+
213+
214+
def _move_status_from_activity_to_tables(conn):
215+
for t in TABLES_TO_MOVE_STATUS:
216+
table_name = t["table"]
217+
enum_name = t["enum"]["name"]
218+
conn.execute(
219+
text(f"""
220+
UPDATE {table_name} t
221+
SET status = a.status::text::{enum_name}
222+
FROM activity a
223+
WHERE a.id = t.id
224+
""")
225+
)
226+
227+
228+
def downgrade() -> None:
229+
conn = op.get_bind()
230+
231+
enums = _create_table_enums(conn)
232+
233+
# create nullable status columns for tables
234+
_create_table_columns(enums)
235+
236+
# copy values from activity to tables
237+
_move_status_from_activity_to_tables(conn)
238+
239+
# remove nullability
240+
for t in TABLES_TO_MOVE_STATUS:
241+
op.alter_column(
242+
t["table"],
243+
t["column"],
244+
existing_type=enums[t["enum"]["name"]],
245+
nullable=False,
246+
)
247+
248+
op.drop_column("activity", "status")
249+
_activity_enum().drop(conn)

app/db/model.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
BIGINT,
3737
JSON_DICT,
3838
STRING_LIST,
39+
ActivityStatus,
3940
ActivityType,
4041
AgentType,
4142
AgePeriod,
@@ -47,7 +48,6 @@
4748
CellMorphologyGenerationType,
4849
CellMorphologyProtocolDesign,
4950
CircuitBuildCategory,
50-
CircuitExtractionExecutionStatus,
5151
CircuitScale,
5252
ContentType,
5353
DerivationType,
@@ -61,17 +61,13 @@
6161
ExecutorType,
6262
ExternalSource,
6363
GlobalType,
64-
IonChannelModelingExecutionStatus,
6564
MeasurementStatistic,
6665
MeasurementUnit,
6766
PointLocation,
6867
PointLocationType,
6968
PublicationType,
7069
RepairPipelineType,
7170
Sex,
72-
SimulationExecutionStatus,
73-
SingleNeuronSimulationStatus,
74-
SkeletonizationExecutionStatus,
7571
SlicingDirectionType,
7672
StainingType,
7773
StorageType,
@@ -424,7 +420,7 @@ class Activity(Identifiable):
424420
authorized_project_id: Mapped[uuid.UUID]
425421
authorized_public: Mapped[bool] = mapped_column(default=False)
426422
type: Mapped[ActivityType]
427-
423+
status: Mapped[ActivityStatus]
428424
start_time: Mapped[datetime | None]
429425
end_time: Mapped[datetime | None]
430426

@@ -1108,7 +1104,6 @@ class SingleNeuronSimulation(LocationMixin, NameDescriptionVectorMixin, Entity):
11081104
seed: Mapped[int]
11091105
injection_location: Mapped[STRING_LIST] = mapped_column(default=[])
11101106
recording_location: Mapped[STRING_LIST] = mapped_column(default=[])
1111-
status: Mapped[SingleNeuronSimulationStatus]
11121107
# TODO: called used ?
11131108
me_model_id: Mapped[uuid.UUID] = mapped_column(
11141109
ForeignKey(f"{EntityType.memodel}.id"), index=True
@@ -1123,7 +1118,6 @@ class SingleNeuronSynaptomeSimulation(LocationMixin, NameDescriptionVectorMixin,
11231118
seed: Mapped[int]
11241119
injection_location: Mapped[STRING_LIST] = mapped_column(default=[])
11251120
recording_location: Mapped[STRING_LIST] = mapped_column(default=[])
1126-
status: Mapped[SingleNeuronSimulationStatus]
11271121
synaptome_id: Mapped[uuid.UUID] = mapped_column(
11281122
ForeignKey(f"{EntityType.single_neuron_synaptome}.id"), index=True
11291123
)
@@ -1347,16 +1341,10 @@ class IonChannelModelingExecution(Activity, ExecutionActivityMixin):
13471341
Attributes:
13481342
id (uuid.UUID): Primary key for the an ion channel modeling execution,
13491343
referencing the ion channel recording IDs.
1350-
status (IonChannelModelingExecutionStatus): The status of the
1351-
ion channel modeling execution.
13521344
"""
13531345

13541346
__tablename__ = ActivityType.ion_channel_modeling_execution.value
13551347
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("activity.id"), primary_key=True)
1356-
status: Mapped[IonChannelModelingExecutionStatus] = mapped_column(
1357-
Enum(IonChannelModelingExecutionStatus, name="ion_channel_modeling_execution_status"),
1358-
default=IonChannelModelingExecutionStatus.created,
1359-
)
13601348

13611349
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
13621350

@@ -1421,7 +1409,7 @@ class Asset(Identifiable):
14211409
"""Asset table."""
14221410

14231411
__tablename__ = "asset"
1424-
status: Mapped[AssetStatus] = mapped_column()
1412+
status: Mapped[AssetStatus] = mapped_column() # TODO: Remove if postgresql_where below removed
14251413
path: Mapped[str] # relative path
14261414
full_path: Mapped[str] # full path on S3
14271415
is_directory: Mapped[bool]
@@ -1586,15 +1574,10 @@ class SimulationExecution(Activity, ExecutionActivityMixin):
15861574
15871575
Attributes:
15881576
id: Primary key for the simulation execution, referencing the entity ID.
1589-
status: The status of the simulation execution.
15901577
"""
15911578

15921579
__tablename__ = ActivityType.simulation_execution.value
15931580
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("activity.id"), primary_key=True)
1594-
status: Mapped[SimulationExecutionStatus] = mapped_column(
1595-
Enum(SimulationExecutionStatus, name="simulation_execution_status"),
1596-
default=SimulationExecutionStatus.created,
1597-
)
15981581

15991582
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
16001583

@@ -1923,18 +1906,13 @@ class CircuitExtractionExecution(Activity, ExecutionActivityMixin):
19231906
19241907
Attributes:
19251908
id (uuid.UUID): Primary key.
1926-
status (CircuitExtractionExecutionStatus): The status of the circuit extraction execution.
19271909
19281910
Note: The CircuitExtractionExecution activity associates a CircuitExtractionConfig entity with
19291911
its corresponding extracted output Circuit entity.
19301912
"""
19311913

19321914
__tablename__ = ActivityType.circuit_extraction_execution.value
19331915
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("activity.id"), primary_key=True)
1934-
status: Mapped[CircuitExtractionExecutionStatus] = mapped_column(
1935-
Enum(CircuitExtractionExecutionStatus, name="circuit_extraction_execution_status"),
1936-
default=CircuitExtractionExecutionStatus.created,
1937-
)
19381916

19391917
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
19401918

@@ -2264,9 +2242,6 @@ class SkeletonizationExecution(Activity, ExecutionActivityMixin):
22642242
__tablename__ = ActivityType.skeletonization_execution.value
22652243

22662244
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("activity.id"), primary_key=True)
2267-
status: Mapped[SkeletonizationExecutionStatus] = mapped_column(
2268-
default=SkeletonizationExecutionStatus.created,
2269-
)
22702245

22712246
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
22722247

0 commit comments

Comments
 (0)