Skip to content

Commit bbbdf04

Browse files
authored
feat(back): add cascade deletion of projects (mlco2#945)
feat(front): add modal to call deletion endpoints for projects
1 parent bcf998c commit bbbdf04

File tree

21 files changed

+1052
-148
lines changed

21 files changed

+1052
-148
lines changed

carbonserver/carbonserver/api/infra/database/sql_models.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Emission(Base):
2121
gpu_energy = Column(Float)
2222
ram_energy = Column(Float)
2323
energy_consumed = Column(Float)
24-
run_id = Column(UUID(as_uuid=True), ForeignKey("runs.id"))
24+
run_id = Column(UUID(as_uuid=True), ForeignKey("runs.id", ondelete="CASCADE"))
2525
run = relationship("Run", back_populates="emissions")
2626

2727
def __repr__(self):
@@ -37,7 +37,9 @@ class Run(Base):
3737
__tablename__ = "runs"
3838
id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
3939
timestamp = Column(DateTime)
40-
experiment_id = Column(UUID(as_uuid=True), ForeignKey("experiments.id"))
40+
experiment_id = Column(
41+
UUID(as_uuid=True), ForeignKey("experiments.id", ondelete="CASCADE")
42+
)
4143
os = Column(String, nullable=True)
4244
python_version = Column(String, nullable=True)
4345
codecarbon_version = Column(String, nullable=True)
@@ -52,7 +54,9 @@ class Run(Base):
5254
ram_total_size = Column(Float, nullable=True)
5355
tracking_mode = Column(String, nullable=True)
5456
experiment = relationship("Experiment", back_populates="runs")
55-
emissions = relationship("Emission", back_populates="run")
57+
emissions = relationship(
58+
"Emission", back_populates="run", cascade="all, delete-orphan"
59+
)
5660

5761
def __repr__(self):
5862
return (
@@ -87,9 +91,13 @@ class Experiment(Base):
8791
on_cloud = Column(Boolean, default=False)
8892
cloud_provider = Column(String)
8993
cloud_region = Column(String)
90-
project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id"))
94+
project_id = Column(
95+
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE")
96+
)
9197
project = relationship("Project", back_populates="experiments")
92-
runs = relationship("Run", back_populates="experiment")
98+
runs = relationship(
99+
"Run", back_populates="experiment", cascade="all, delete-orphan"
100+
)
93101

94102
def __repr__(self):
95103
return (
@@ -111,9 +119,13 @@ class Project(Base):
111119
description = Column(String)
112120
public = Column(Boolean, default=False)
113121
organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"))
114-
experiments = relationship("Experiment", back_populates="project")
122+
experiments = relationship(
123+
"Experiment", back_populates="project", cascade="all, delete-orphan"
124+
)
115125
organization = relationship("Organization", back_populates="projects")
116-
project_tokens = relationship("ProjectToken", back_populates="project")
126+
project_tokens = relationship(
127+
"ProjectToken", back_populates="project", cascade="all, delete-orphan"
128+
)
117129

118130
def __repr__(self):
119131
return (
@@ -176,7 +188,9 @@ def __repr__(self):
176188
class ProjectToken(Base):
177189
__tablename__ = "project_tokens"
178190
id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
179-
project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id"))
191+
project_id = Column(
192+
UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE")
193+
)
180194
name = Column(String)
181195
hashed_token = Column(String, nullable=False)
182196
lookup_value = Column(
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""add_cascade_delete_for_projects
2+
3+
Revision ID: 2a898cf81c3e
4+
Revises: f3a10c95079f
5+
Create Date: 2025-10-05 11:40:28.037992
6+
7+
"""
8+
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "2a898cf81c3e"
13+
down_revision = "f3a10c95079f"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
"""
20+
Add CASCADE delete behavior to foreign keys in the project hierarchy.
21+
22+
When a Project is deleted, this will automatically delete:
23+
- All Experiments belonging to that project
24+
- All Runs belonging to those experiments
25+
- All Emissions belonging to those runs
26+
- All ProjectTokens belonging to that project
27+
"""
28+
29+
# 1. Add CASCADE delete to emissions.run_id foreign key
30+
# Drop existing constraint (using the name from initial migration)
31+
op.drop_constraint("fk_emissions_runs", "emissions", type_="foreignkey")
32+
# Recreate with CASCADE
33+
op.create_foreign_key(
34+
"fk_emissions_runs", "emissions", "runs", ["run_id"], ["id"], ondelete="CASCADE"
35+
)
36+
37+
# 2. Add CASCADE delete to runs.experiment_id foreign key
38+
op.drop_constraint("fk_runs_experiments", "runs", type_="foreignkey")
39+
op.create_foreign_key(
40+
"fk_runs_experiments",
41+
"runs",
42+
"experiments",
43+
["experiment_id"],
44+
["id"],
45+
ondelete="CASCADE",
46+
)
47+
48+
# 3. Add CASCADE delete to experiments.project_id foreign key
49+
op.drop_constraint("fk_experiments_projects", "experiments", type_="foreignkey")
50+
op.create_foreign_key(
51+
"fk_experiments_projects",
52+
"experiments",
53+
"projects",
54+
["project_id"],
55+
["id"],
56+
ondelete="CASCADE",
57+
)
58+
59+
# 4. Add CASCADE delete to project_tokens.project_id foreign key
60+
op.drop_constraint(
61+
"fk_project_tokens_projects", "project_tokens", type_="foreignkey"
62+
)
63+
op.create_foreign_key(
64+
"fk_project_tokens_projects",
65+
"project_tokens",
66+
"projects",
67+
["project_id"],
68+
["id"],
69+
ondelete="CASCADE",
70+
)
71+
72+
73+
def downgrade():
74+
"""
75+
Remove CASCADE delete behavior from foreign keys.
76+
This restores the original behavior where deleting a project with
77+
child records will fail with a foreign key constraint violation.
78+
"""
79+
80+
# 4. Remove CASCADE from project_tokens.project_id
81+
op.drop_constraint(
82+
"fk_project_tokens_projects", "project_tokens", type_="foreignkey"
83+
)
84+
op.create_foreign_key(
85+
"fk_project_tokens_projects",
86+
"project_tokens",
87+
"projects",
88+
["project_id"],
89+
["id"],
90+
)
91+
92+
# 3. Remove CASCADE from experiments.project_id
93+
op.drop_constraint("fk_experiments_projects", "experiments", type_="foreignkey")
94+
op.create_foreign_key(
95+
"fk_experiments_projects", "experiments", "projects", ["project_id"], ["id"]
96+
)
97+
98+
# 2. Remove CASCADE from runs.experiment_id
99+
op.drop_constraint("fk_runs_experiments", "runs", type_="foreignkey")
100+
op.create_foreign_key(
101+
"fk_runs_experiments", "runs", "experiments", ["experiment_id"], ["id"]
102+
)
103+
104+
# 1. Remove CASCADE from emissions.run_id
105+
op.drop_constraint("fk_emissions_runs", "emissions", type_="foreignkey")
106+
op.create_foreign_key("fk_emissions_runs", "emissions", "runs", ["run_id"], ["id"])
Lines changed: 122 additions & 0 deletions
Loading

carbonserver/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ Changelog = "https://github.com/mlco2/codecarbon/releases"
5151

5252
[tool.hatch.build]
5353
exclude = ["*"]
54+
55+
[tool.pytest.ini_options]
56+
pythonpath = "."

0 commit comments

Comments
 (0)