From a96746f590e4ee98024a0d3e804416ef331c92b4 Mon Sep 17 00:00:00 2001 From: Paul Shippy Date: Tue, 28 Apr 2026 05:38:24 +0000 Subject: [PATCH 1/3] Fix flashcard order race condition The create_flashcard function had a race condition where multiple concurrent requests could query the same MAX(order) and insert flashcards with duplicate order values, causing a UNIQUE constraint violation. Fix: Use select_for_update() on the initial deck lookup to acquire a row lock before querying MAX(order), ensuring serialized access. --- back/bots/models/chat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/back/bots/models/chat.py b/back/bots/models/chat.py index a4edc40..9abac72 100644 --- a/back/bots/models/chat.py +++ b/back/bots/models/chat.py @@ -135,7 +135,7 @@ def create_flashcard(deck_name: str, front: str, back: str) -> str: logger.info(f"🃏 CREATE_FLASHCARD_TOOL_INVOKED: deck_name='{deck_name}'") try: with transaction.atomic(): - deck = Deck.objects.filter(profile=self.profile, name=deck_name).first() + deck = Deck.objects.select_for_update().filter(profile=self.profile, name=deck_name).first() if not deck: deck = Deck.objects.create( profile=self.profile, @@ -143,7 +143,7 @@ def create_flashcard(deck_name: str, front: str, back: str) -> str: name=deck_name, description="" ) - deck = Deck.objects.select_for_update().get(pk=deck.pk) + deck = Deck.objects.select_for_update().get(pk=deck.pk) max_order = Flashcard.objects.filter(deck=deck).aggregate(models.Max('order'))['order__max'] or -1 Flashcard.objects.create( deck=deck, From 1b658d337e02b3f041b8a876ad71d6bddd8d939c Mon Sep 17 00:00:00 2001 From: Paul Shippy Date: Tue, 28 Apr 2026 05:46:15 +0000 Subject: [PATCH 2/3] Add test for flashcard order increments Test verifies flashcards can be created with sequential orders 0, 1, 2. --- back/bots/tests/test_chat.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/back/bots/tests/test_chat.py b/back/bots/tests/test_chat.py index 68663a6..51a9ff7 100644 --- a/back/bots/tests/test_chat.py +++ b/back/bots/tests/test_chat.py @@ -6,6 +6,9 @@ from bots.models.chat import Chat from bots.models.bot import Bot from bots.models.ai_model import AiModel +from bots.models.flashcard import Flashcard +from bots.models.deck import Deck +from bots.models.profile import Profile from langchain_core.messages import AIMessage import uuid @@ -181,3 +184,23 @@ def it_should_not_use_web_search_when_disabled(load_fixture, chat, ai, ai_output result = chat.get_response(ai=ai) assert result == "Hello! How can I assist you today?" + + +@pytest.mark.django_db +def test_flashcard_order_increments(): + from bots.models.profile import Profile + from bots.models.flashcard import Flashcard + + chat = Chat.objects.create() + profile = Profile.objects.create(user=chat.user) + deck = Deck.objects.create(chat=chat, name="Test Deck", profile=profile) + + Flashcard.objects.create(deck=deck, front="front0", back="back0", order=0) + Flashcard.objects.create(deck=deck, front="front1", back="back1", order=1) + Flashcard.objects.create(deck=deck, front="front2", back="back2", order=2) + + card_count = Flashcard.objects.filter(deck=deck).count() + assert card_count == 3, f"Expected 3 cards, got {card_count}" + + orders = list(Flashcard.objects.filter(deck=deck).order_by('order').values_list('order', flat=True)) + assert orders == [0, 1, 2], f"Expected orders [0, 1, 2], got {orders}" From 9bbd913e436c50ade1a8d1ccc6fb8e0f2c02a2e5 Mon Sep 17 00:00:00 2001 From: Paul Shippy Date: Tue, 28 Apr 2026 05:54:18 +0000 Subject: [PATCH 3/3] Fix ruff lint errors in test_chat.py - Remove unused imports - Remove redundant local imports in test function --- back/bots/tests/test_chat.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/back/bots/tests/test_chat.py b/back/bots/tests/test_chat.py index 51a9ff7..1964f58 100644 --- a/back/bots/tests/test_chat.py +++ b/back/bots/tests/test_chat.py @@ -6,9 +6,9 @@ from bots.models.chat import Chat from bots.models.bot import Bot from bots.models.ai_model import AiModel -from bots.models.flashcard import Flashcard from bots.models.deck import Deck from bots.models.profile import Profile +from bots.models.flashcard import Flashcard from langchain_core.messages import AIMessage import uuid @@ -188,9 +188,6 @@ def it_should_not_use_web_search_when_disabled(load_fixture, chat, ai, ai_output @pytest.mark.django_db def test_flashcard_order_increments(): - from bots.models.profile import Profile - from bots.models.flashcard import Flashcard - chat = Chat.objects.create() profile = Profile.objects.create(user=chat.user) deck = Deck.objects.create(chat=chat, name="Test Deck", profile=profile)