Audyt zapytań bez limitu (.all())
Dokument opisuje, które zapytania do D1 zwracają nieograniczoną liczbę wierszy, jak oceniamy ich ryzyko i jakie limity obowiązują. Powstał w ramach AP-784 (ograniczanie zużycia pamięci Workera, limit 128 MB na isolate).
👤 Dla zespołu — co to zmienia w praktyce
- Wyszukiwarka klientów wymaga co najmniej 2 znaków. Wpisanie jednej litery nie zwraca już wyników — wcześniej takie zapytanie przeszukiwało całą bazę graczy, żeby zwrócić listę, z której i tak nie dało się skorzystać.
- Lista wyników wyszukiwania jest przycięta do 20 pozycji (maksymalnie 50). Jeżeli szukanej osoby nie ma na liście, doprecyzuj frazę zamiast przewijać.
- Tablica zgłoszeń (tickets) pokazuje maksymalnie 500 najnowszych zgłoszeń. Przy większej liczbie użyj filtrów statusu lub wyszukiwania.
🛠️ Dokumentacja techniczna
Obowiązujące limity
| Miejsce | Stała | Wartość |
|---|---|---|
| Minimalna długość frazy wyszukiwania graczy | PLAYER_SEARCH_MIN_LENGTH | 2 znaki |
| Domyślny limit wyszukiwarki graczy | PLAYER_SEARCH_DEFAULT_LIMIT | 20 |
| Maksymalny limit wyszukiwarki graczy | PLAYER_SEARCH_MAX_LIMIT | 50 |
| Maksymalna strona listy graczy | PLAYER_PAGE_MAX_LIMIT | 200 |
| Maksymalna liczba zgłoszeń na tablicy | TICKETS_MAX_ROWS | 500 |
| Strona odbiorców kampanii mailowej | RECIPIENT_PAGE_SIZE | 500 |
Limity podane przez wywołującego są przycinane (Math.min), a nie przyjmowane na wiarę — dzięki temu żaden komponent nie obejdzie zabezpieczenia, przekazując Number.MAX_SAFE_INTEGER.
Metodyka audytu
Przeskanowano wszystkie wywołania db.prepare(...).all() w lib/actions/. Z 167 zapytań SELECT bez klauzuli LIMIT wydzielono te, które nie są ograniczone predykatem klucza (id = ?, player_id = ?, IN (${placeholders}) itd.) — pozostały 72 zapytania filtrowane wyłącznie zakresowo (tenant_id, status, zakres dat).
Kluczowa obserwacja: brak LIMIT sam w sobie nie jest wadą. Ryzyko zależy od tego, czy liczba wierszy rośnie w czasie. Dlatego stosunek .all() do LIMIT (użyty jako heurystyka w opisie zadania) okazał się mylący — np. invoices.ts ma 8 wywołań .all() i 2 LIMIT, ale każde z nich jest ograniczone przez player_id = ? lub owner_email = ? i zwraca pojedyncze wiersze.
Klasyfikacja
Do naprawy — naprawione w AP-784
| Miejsce | Problem | Rozwiązanie |
|---|---|---|
players.ts getPlayersByName | LIKE po 12 kolumnach; fraza 1-znakowa skanowała całą tabelę | minimalna długość frazy + przycięcie limitu |
players.ts getPlayersWithExtensions | pageLimit przyjmowany bez ograniczeń, a podzapytania json_group_array budują tablicę JSON na każdy wiersz | przycięcie do PLAYER_PAGE_MAX_LIMIT |
players.ts getAllOwnPlayers / getAllOwnPlayersIncludingInactive | pageLimit: Number.MAX_SAFE_INTEGER | jawny limit 200 (zapytanie i tak zawężone do owner_email bieżącego użytkownika) |
tickets.ts getTickets | pełna lista zgłoszeń tenanta bez LIMIT, plus zbiorcze pobranie etykiet i przypisań po wszystkich id | LIMIT 500 |
Świadomie dopuszczone — tabele konfiguracyjne o niskiej liczności
Rosną z konfiguracją klubu, nie z ruchem ani z upływem czasu. Dodanie paginacji pogorszyłoby czytelność bez zysku.
court.ts—SELECT * FROM court,DISTINCT city / surface / street(dziesiątki wierszy)activity-types.ts— lista typów zajęć i kategoriiapp-settings.ts,app-settings-general.ts— ustawienia,DISTINCT city / categoryroute-access.ts— reguły dostępu do traschecklist.ts— definicje zadań (checklist_task)quality.ts— lista pracownikówmail-campaigns.ts—mail_segment,mail_campaign(jednostki na tenanta)
Świadomie dopuszczone — ograniczone zakresem biznesowym
Liczba wierszy wynika z rozmiaru encji nadrzędnej, nie z wieku bazy.
camp-*.ts,tennis-course-*.ts— rejestracje, lista oczekujących i instruktorzy w obrębie jednego turnusu/sezonu (limit miejsc)game.ts— zapytania porecurring_series_id(liczba wystąpień serii w sezonie)membership.tsgetExpiringMemberships— ograniczone oknem datchecklist.tschecklist_completion— ograniczone zakresem datplayers.tsappendSuspensionStatusBatch— przetwarzane porcjami po 90 adresówactivity-log.ts— gałęzie zapytania korzystają zfetchCapstatistics.ts— zapytania agregujące (GROUP BY) zwracają pojedyncze wiersze podsumowań
Obserwowane — bez zmian, do rewizji przy wzroście
critical-notifications.ts—WHERE is_resolved = 0: ograniczone liczbą nierozwiązanych powiadomień. Bezpieczne dopóki są rozwiązywane; przy zaniedbaniu obsługi może rosnąć bez ograniczeń.
Zasada dla nowego kodu
Przed dodaniem .all() zadaj pytanie: czy liczba wierszy rośnie z czasem lub z liczbą klientów? Jeśli tak, zapytanie musi mieć LIMIT albo predykat klucza. Jeśli nie (tabela konfiguracyjna, zakres jednej encji) — dopisz je do sekcji „świadomie dopuszczone" powyżej.