Skip to content

Commit 671d384

Browse files
authored
Migrate User Model ID from UUID to Integer (#219)
* database migration for user_id to int * changes in api and crud * fix test cases * fix migration * run precommit * fix migration head * pre commit run * drop unnecessary migration
1 parent d6d5ead commit 671d384

24 files changed

+275
-51
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""user_id from uuid to int
2+
3+
Revision ID: 66abc97f3782
4+
Revises: 60b6c511a485
5+
Create Date: 2025-06-16 11:05:36.196795
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel.sql.sqltypes
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "66abc97f3782"
15+
down_revision = "60b6c511a485"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
conn = op.get_bind()
22+
23+
# Drop foreign key constraints
24+
conn.execute(
25+
sa.text("ALTER TABLE document DROP CONSTRAINT document_owner_id_fkey;")
26+
)
27+
conn.execute(
28+
sa.text("ALTER TABLE collection DROP CONSTRAINT collection_owner_id_fkey;")
29+
)
30+
conn.execute(
31+
sa.text("ALTER TABLE projectuser DROP CONSTRAINT projectuser_user_id_fkey;")
32+
)
33+
conn.execute(sa.text("ALTER TABLE apikey DROP CONSTRAINT apikey_user_id_fkey;"))
34+
35+
# Drop primary key constraint on "user" table
36+
conn.execute(sa.text('ALTER TABLE "user" DROP CONSTRAINT user_pkey;'))
37+
38+
# Create mapping table from UUID to INT
39+
conn.execute(
40+
sa.text(
41+
"""
42+
CREATE TABLE uuid_to_int_map (
43+
user_id_uuid UUID PRIMARY KEY,
44+
user_id_int INT GENERATED ALWAYS AS IDENTITY
45+
);
46+
"""
47+
)
48+
)
49+
50+
# Populate mapping table
51+
conn.execute(
52+
sa.text('INSERT INTO uuid_to_int_map (user_id_uuid) SELECT id FROM "user";')
53+
)
54+
55+
# Add new_id to user table and populate it
56+
conn.execute(sa.text('ALTER TABLE "user" ADD COLUMN new_id INT;'))
57+
conn.execute(
58+
sa.text(
59+
"""
60+
UPDATE "user" SET new_id = uuid_map.user_id_int
61+
FROM uuid_to_int_map uuid_map
62+
WHERE "user".id = uuid_map.user_id_uuid;
63+
"""
64+
)
65+
)
66+
67+
# document
68+
conn.execute(sa.text("ALTER TABLE document ADD COLUMN new_owner_id INT;"))
69+
conn.execute(
70+
sa.text(
71+
"""
72+
UPDATE document SET new_owner_id = uuid_map.user_id_int
73+
FROM uuid_to_int_map uuid_map
74+
WHERE document.owner_id = uuid_map.user_id_uuid;
75+
"""
76+
)
77+
)
78+
79+
# collection
80+
conn.execute(sa.text("ALTER TABLE collection ADD COLUMN new_owner_id INT;"))
81+
conn.execute(
82+
sa.text(
83+
"""
84+
UPDATE collection SET new_owner_id = uuid_map.user_id_int
85+
FROM uuid_to_int_map uuid_map
86+
WHERE collection.owner_id = uuid_map.user_id_uuid;
87+
"""
88+
)
89+
)
90+
91+
# projectuser
92+
conn.execute(sa.text("ALTER TABLE projectuser ADD COLUMN new_user_id INT;"))
93+
conn.execute(
94+
sa.text(
95+
"""
96+
UPDATE projectuser SET new_user_id = uuid_map.user_id_int
97+
FROM uuid_to_int_map uuid_map
98+
WHERE projectuser.user_id = uuid_map.user_id_uuid;
99+
"""
100+
)
101+
)
102+
103+
# apikey
104+
conn.execute(sa.text("ALTER TABLE apikey ADD COLUMN new_user_id INT;"))
105+
conn.execute(
106+
sa.text(
107+
"""
108+
UPDATE apikey SET new_user_id = uuid_map.user_id_int
109+
FROM uuid_to_int_map uuid_map
110+
WHERE apikey.user_id = uuid_map.user_id_uuid;
111+
"""
112+
)
113+
)
114+
115+
# Drop old columns and rename new ones
116+
conn.execute(sa.text("ALTER TABLE document DROP COLUMN owner_id;"))
117+
conn.execute(
118+
sa.text("ALTER TABLE document RENAME COLUMN new_owner_id TO owner_id;")
119+
)
120+
121+
conn.execute(sa.text("ALTER TABLE collection DROP COLUMN owner_id;"))
122+
conn.execute(
123+
sa.text("ALTER TABLE collection RENAME COLUMN new_owner_id TO owner_id;")
124+
)
125+
126+
conn.execute(sa.text("ALTER TABLE projectuser DROP COLUMN user_id;"))
127+
conn.execute(
128+
sa.text("ALTER TABLE projectuser RENAME COLUMN new_user_id TO user_id;")
129+
)
130+
131+
conn.execute(sa.text("ALTER TABLE apikey DROP COLUMN user_id;"))
132+
conn.execute(sa.text("ALTER TABLE apikey RENAME COLUMN new_user_id TO user_id;"))
133+
134+
conn.execute(sa.text('ALTER TABLE "user" DROP COLUMN id;'))
135+
conn.execute(sa.text('ALTER TABLE "user" RENAME COLUMN new_id TO id;'))
136+
137+
# Re-add primary key
138+
conn.execute(sa.text('ALTER TABLE "user" ADD PRIMARY KEY (id);'))
139+
140+
# Create sequence for new integer IDs
141+
conn.execute(sa.text('CREATE SEQUENCE user_id_seq START 1 OWNED BY "user".id;'))
142+
conn.execute(
143+
sa.text(
144+
"ALTER TABLE \"user\" ALTER COLUMN id SET DEFAULT nextval('user_id_seq');"
145+
)
146+
)
147+
conn.execute(
148+
sa.text("SELECT setval('user_id_seq', (SELECT MAX(id) FROM \"user\"));")
149+
)
150+
151+
# Drop mapping table
152+
conn.execute(sa.text("DROP TABLE uuid_to_int_map;"))
153+
154+
155+
def downgrade():
156+
pass
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""add fk constraint to user id columns
2+
3+
Revision ID: 8e7dc5eab0b0
4+
Revises: 66abc97f3782
5+
Create Date: 2025-06-16 11:08:46.893642
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel.sql.sqltypes
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "8e7dc5eab0b0"
15+
down_revision = "66abc97f3782"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.alter_column("apikey", "user_id", existing_type=sa.INTEGER(), nullable=False)
23+
op.create_foreign_key(
24+
None, "apikey", "user", ["user_id"], ["id"], ondelete="CASCADE"
25+
)
26+
op.alter_column(
27+
"collection", "owner_id", existing_type=sa.INTEGER(), nullable=False
28+
)
29+
op.create_foreign_key(
30+
None, "collection", "user", ["owner_id"], ["id"], ondelete="CASCADE"
31+
)
32+
op.alter_column("document", "owner_id", existing_type=sa.INTEGER(), nullable=False)
33+
op.create_foreign_key(
34+
None, "document", "user", ["owner_id"], ["id"], ondelete="CASCADE"
35+
)
36+
op.alter_column(
37+
"projectuser", "user_id", existing_type=sa.INTEGER(), nullable=False
38+
)
39+
op.create_foreign_key(
40+
None, "projectuser", "user", ["user_id"], ["id"], ondelete="CASCADE"
41+
)
42+
# ### end Alembic commands ###
43+
44+
45+
def downgrade():
46+
# ### commands auto generated by Alembic - please adjust! ###
47+
op.drop_constraint(None, "projectuser", type_="foreignkey")
48+
op.alter_column("projectuser", "user_id", existing_type=sa.INTEGER(), nullable=True)
49+
op.drop_constraint(None, "document", type_="foreignkey")
50+
op.alter_column("document", "owner_id", existing_type=sa.INTEGER(), nullable=True)
51+
op.drop_constraint(None, "collection", type_="foreignkey")
52+
op.alter_column("collection", "owner_id", existing_type=sa.INTEGER(), nullable=True)
53+
op.drop_constraint(None, "apikey", type_="foreignkey")
54+
op.alter_column("apikey", "user_id", existing_type=sa.INTEGER(), nullable=True)
55+
# ### end Alembic commands ###

backend/app/api/routes/api_keys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
@router.post("/", response_model=APIResponse[APIKeyPublic])
2121
def create_key(
2222
project_id: int,
23-
user_id: uuid.UUID,
23+
user_id: int,
2424
session: Session = Depends(get_db),
2525
current_user: User = Depends(get_current_active_superuser),
2626
):

backend/app/api/routes/onboarding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class OnboardingRequest(BaseModel):
4040
class OnboardingResponse(BaseModel):
4141
organization_id: int
4242
project_id: int
43-
user_id: uuid.UUID
43+
user_id: int
4444
api_key: str
4545

4646

backend/app/api/routes/project_user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
)
2323
def add_user(
2424
request: Request,
25-
user_id: uuid.UUID,
25+
user_id: int,
2626
is_admin: bool = False,
2727
session: Session = Depends(get_db),
2828
current_user: UserProjectOrg = Depends(verify_user_project_organization),
@@ -81,7 +81,7 @@ def list_project_users(
8181
)
8282
def remove_user(
8383
request: Request,
84-
user_id: uuid.UUID,
84+
user_id: int,
8585
session: Session = Depends(get_db),
8686
current_user: UserProjectOrg = Depends(verify_user_project_organization),
8787
):

backend/app/api/routes/users.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def register_user(session: SessionDep, user_in: UserRegister) -> Any:
160160

161161
@router.get("/{user_id}", response_model=UserPublic, include_in_schema=False)
162162
def read_user_by_id(
163-
user_id: uuid.UUID, session: SessionDep, current_user: CurrentUser
163+
user_id: int, session: SessionDep, current_user: CurrentUser
164164
) -> Any:
165165
"""
166166
Get a specific user by id.
@@ -185,7 +185,7 @@ def read_user_by_id(
185185
def update_user_endpoint(
186186
*,
187187
session: SessionDep,
188-
user_id: uuid.UUID,
188+
user_id: int,
189189
user_in: UserUpdate,
190190
) -> Any:
191191
"""
@@ -215,7 +215,7 @@ def update_user_endpoint(
215215
include_in_schema=False,
216216
)
217217
def delete_user(
218-
session: SessionDep, current_user: CurrentUser, user_id: uuid.UUID
218+
session: SessionDep, current_user: CurrentUser, user_id: int
219219
) -> Message:
220220
"""
221221
Delete a user.

backend/app/crud/api_key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def generate_api_key() -> tuple[str, str]:
1919

2020

2121
def create_api_key(
22-
session: Session, organization_id: int, user_id: uuid.UUID, project_id: int
22+
session: Session, organization_id: int, user_id: int, project_id: int
2323
) -> APIKeyPublic:
2424
"""
2525
Generates a new API key for an organization and associates it with a user.

backend/app/crud/collection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
class CollectionCrud:
14-
def __init__(self, session: Session, owner_id: UUID):
14+
def __init__(self, session: Session, owner_id: int):
1515
self.session = session
1616
self.owner_id = owner_id
1717

backend/app/crud/document.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
class DocumentCrud:
11-
def __init__(self, session: Session, owner_id: UUID):
11+
def __init__(self, session: Session, owner_id: int):
1212
self.session = session
1313
self.owner_id = owner_id
1414

backend/app/crud/project_user.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from app.core.util import now
77

88

9-
def is_project_admin(session: Session, user_id: str, project_id: int) -> bool:
9+
def is_project_admin(session: Session, user_id: int, project_id: int) -> bool:
1010
"""
1111
Checks if a user is an admin of the given project.
1212
"""
@@ -23,7 +23,7 @@ def is_project_admin(session: Session, user_id: str, project_id: int) -> bool:
2323

2424
# Add a user to a project
2525
def add_user_to_project(
26-
session: Session, project_id: uuid.UUID, user_id: uuid.UUID, is_admin: bool = False
26+
session: Session, project_id: int, user_id: int, is_admin: bool = False
2727
) -> ProjectUserPublic:
2828
"""
2929
Adds a user to a project.
@@ -47,9 +47,7 @@ def add_user_to_project(
4747
return ProjectUserPublic.model_validate(project_user)
4848

4949

50-
def remove_user_from_project(
51-
session: Session, project_id: uuid.UUID, user_id: uuid.UUID
52-
) -> None:
50+
def remove_user_from_project(session: Session, project_id: int, user_id: int) -> None:
5351
"""
5452
Removes a user from a project.
5553
"""
@@ -70,7 +68,7 @@ def remove_user_from_project(
7068

7169

7270
def get_users_by_project(
73-
session: Session, project_id: uuid.UUID, skip: int = 0, limit: int = 100
71+
session: Session, project_id: int, skip: int = 0, limit: int = 100
7472
) -> tuple[list[ProjectUserPublic], int]:
7573
"""
7674
Returns paginated users in a given project along with the total count.
@@ -94,9 +92,7 @@ def get_users_by_project(
9492

9593

9694
# Check if a user belongs to an at least one project in organization
97-
def is_user_part_of_organization(
98-
session: Session, user_id: uuid.UUID, org_id: int
99-
) -> bool:
95+
def is_user_part_of_organization(session: Session, user_id: int, org_id: int) -> bool:
10096
"""
10197
Checks if a user is part of at least one project within the organization.
10298
"""

0 commit comments

Comments
 (0)