From 18394aa74ef287166d22d347e388f4bf67f3eae9 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 27 Mar 2026 01:03:40 -0500 Subject: [PATCH 1/2] fix: align CustomCommandCreator.discord_id model with BIGINT column (#78) Closes #78 Change CharField(max_length=20) to BigIntegerField to match the BIGINT column created by the migration. Remove the str() workaround in get_creator_by_discord_id() that was compensating for the type mismatch. Co-Authored-By: Claude Sonnet 4.6 --- app/db_engine.py | 3 +-- app/routers_v3/custom_commands.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/db_engine.py b/app/db_engine.py index 53aa542..0e6790c 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -2463,8 +2463,7 @@ class Decision(BaseModel): class CustomCommandCreator(BaseModel): """Model for custom command creators.""" - - discord_id = CharField(max_length=20, unique=True) # Discord snowflake ID as string + discord_id = BigIntegerField(unique=True) username = CharField(max_length=32) display_name = CharField(max_length=32, null=True) created_at = DateTimeField() diff --git a/app/routers_v3/custom_commands.py b/app/routers_v3/custom_commands.py index c398589..4e50cb2 100644 --- a/app/routers_v3/custom_commands.py +++ b/app/routers_v3/custom_commands.py @@ -175,7 +175,7 @@ def delete_custom_command(command_id: int): def get_creator_by_discord_id(discord_id: int): """Get a creator by Discord ID""" creator = CustomCommandCreator.get_or_none( - CustomCommandCreator.discord_id == str(discord_id) + CustomCommandCreator.discord_id == discord_id ) if creator: return model_to_dict(creator) From e311de93aa609fb112beb524d6dd2335654bd369 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 5 Mar 2026 16:03:28 -0600 Subject: [PATCH 2/2] fix: eliminate N+1 queries in get_custom_commands (#26) Expand the JOIN SELECT to include all creator fields (created_at, total_commands, active_commands) and build the CustomCommandCreatorModel directly from joined row data instead of issuing a separate SELECT per command. Reduces 26 queries to 2 for a full 25-command page. Co-Authored-By: Claude Sonnet 4.6 --- app/routers_v3/custom_commands.py | 45 +++++++++++++++++-------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/app/routers_v3/custom_commands.py b/app/routers_v3/custom_commands.py index 4e50cb2..617cebf 100644 --- a/app/routers_v3/custom_commands.py +++ b/app/routers_v3/custom_commands.py @@ -273,7 +273,10 @@ async def get_custom_commands( sql = f""" SELECT cc.*, creator.discord_id as creator_discord_id, creator.username as creator_username, - creator.display_name as creator_display_name + creator.display_name as creator_display_name, + creator.created_at as creator_created_at, + creator.total_commands as creator_total_commands, + creator.active_commands as creator_active_commands FROM custom_commands cc LEFT JOIN custom_command_creators creator ON cc.creator_id = creator.id {where_clause} @@ -298,24 +301,23 @@ async def get_custom_commands( command_dict["tags"] = json.loads(command_dict["tags"]) except Exception: command_dict["tags"] = [] - # Get full creator information - creator_id = command_dict["creator_id"] - creator_cursor = db.execute_sql( - "SELECT * FROM custom_command_creators WHERE id = %s", (creator_id,) - ) - creator_result = creator_cursor.fetchone() - if creator_result: - # Create complete creator object - creator_columns = [desc[0] for desc in creator_cursor.description] - creator_dict = dict(zip(creator_columns, creator_result)) - # Convert datetime to ISO string - if creator_dict.get("created_at") and hasattr( - creator_dict["created_at"], "isoformat" - ): - creator_dict["created_at"] = creator_dict[ - "created_at" - ].isoformat() + # Build creator object from joined data (avoids N+1 queries) + if command_dict.get("creator_discord_id") is not None: + creator_created_at = command_dict.get("creator_created_at") + if creator_created_at and hasattr(creator_created_at, "isoformat"): + creator_created_at = creator_created_at.isoformat() + creator_dict = { + "id": command_dict["creator_id"], + "discord_id": command_dict["creator_discord_id"], + "username": command_dict["creator_username"], + "display_name": command_dict.get("creator_display_name"), + "created_at": creator_created_at, + "total_commands": command_dict.get("creator_total_commands", 0), + "active_commands": command_dict.get( + "creator_active_commands", 0 + ), + } try: creator_model = CustomCommandCreatorModel(**creator_dict) command_dict["creator"] = creator_model @@ -325,13 +327,15 @@ async def get_custom_commands( ) command_dict["creator"] = None else: - # No creator found, set to None command_dict["creator"] = None - # Remove the individual creator fields now that we have the creator object + # Remove joined creator fields before building the command model command_dict.pop("creator_discord_id", None) command_dict.pop("creator_username", None) command_dict.pop("creator_display_name", None) + command_dict.pop("creator_created_at", None) + command_dict.pop("creator_total_commands", None) + command_dict.pop("creator_active_commands", None) # Convert datetime fields to ISO strings for field in ["created_at", "updated_at", "last_used"]: @@ -1055,3 +1059,4 @@ async def get_custom_command(command_id: int): except Exception as e: logger.error(f"Error getting custom command {command_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) +