--- phase: 07-pre-expansion-fixes plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/Modules/Orders/OrdersRepository.php - database/migrations/20260313_000048_add_orders_performance_indexes.sql autonomous: true --- ## Goal Wyeliminować dwa bottlenecki wydajnościowe w module Orders: 4 correlated subqueries na każdy wiersz listy zamówień oraz zapytanie do information_schema przy każdym żądaniu HTTP. ## Purpose Lista zamówień jest głównym widokiem aplikacji. Przy 50 wierszach na stronę, correlated subqueries oznaczają 200+ dodatkowych zapytań per page load. `canResolveMappedMedia()` uderza w `information_schema` przy każdym żądaniu do OrdersRepository — notoriously slow na MySQL. Oba problemy narastają z liczbą zamówień. ## Output - `OrdersRepository::buildListSql()` — 4 subqueries zastąpione aggregating LEFT JOIN - `OrdersRepository::canResolveMappedMedia()` — wynik cachowany w `static` property - Nowa migracja z brakującymi indeksami dla `orders` table ## Project Context @.paul/PROJECT.md ## Source Files @src/Modules/Orders/OrdersRepository.php ## AC-1: Brak correlated subqueries w liście zamówień ```gherkin Given OrdersRepository::buildListSql() zawiera 4 correlated subqueries (items_count, items_qty, shipments_count, documents_count) When metoda buduje SQL dla listy zamówień Then SQL nie zawiera "(SELECT COUNT(*) FROM order_items" ani podobnych correlated subqueries AND zwracane kolumny items_count, items_qty, shipments_count, documents_count nadal istnieją AND logika transformOrderRow() w pełni działa (te pola nadal trafiają do wyniku) ``` ## AC-2: information_schema nie jest odpytywany per-request ```gherkin Given canResolveMappedMedia() używa instance property $this->supportsMappedMedia jako cache When OrdersRepository jest instanciowany dwukrotnie w tej samej sesji PHP (np. dwa wywołania repository) Then information_schema.COLUMNS jest zapytany co najwyżej raz na cykl PHP (static property) ``` ## AC-3: Brakujące indeksy dodane migracją ```gherkin Given tabela orders nie ma indeksów na kolumnach source, external_status_id, ordered_at When tworzona jest nowa migracja i wykonana przez migrator Then tabela orders ma indeksy na: source, external_status_id, ordered_at AND migracja jest idempotentna (IF NOT EXISTS lub ADD INDEX IF NOT EXISTS lub ALTER IGNORE) ``` Task 1: Zamień 4 correlated subqueries na aggregating LEFT JOINs w buildListSql() src/Modules/Orders/OrdersRepository.php W metodzie `buildListSql()` (linie ~135-171) zamień: ```sql (SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count, (SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty, (SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count, (SELECT COUNT(*) FROM order_documents od WHERE od.order_id = o.id) AS documents_count ``` Na: ```sql COALESCE(oi_agg.items_count, 0) AS items_count, COALESCE(oi_agg.items_qty, 0) AS items_qty, COALESCE(sh_agg.shipments_count, 0) AS shipments_count, COALESCE(od_agg.documents_count, 0) AS documents_count ``` I dodaj do klauzuli FROM (po istniejących LEFT JOINach, przed WHERE): ```sql LEFT JOIN ( SELECT order_id, COUNT(*) AS items_count, COALESCE(SUM(quantity), 0) AS items_qty FROM order_items GROUP BY order_id ) oi_agg ON oi_agg.order_id = o.id LEFT JOIN ( SELECT order_id, COUNT(*) AS shipments_count FROM order_shipments GROUP BY order_id ) sh_agg ON sh_agg.order_id = o.id LEFT JOIN ( SELECT order_id, COUNT(*) AS documents_count FROM order_documents GROUP BY order_id ) od_agg ON od_agg.order_id = o.id ``` UWAGA: buildListSql() generuje SQL jako string — upewnij się że nowe JOINy są wstawione przed `$whereSql` (który jest konkatenowany na końcu z klauzulą WHERE). Sprawdź jak wygląda $whereSql — czy zawiera "WHERE" czy tylko "AND ..."? Jeśli WHERE jest w $whereSql, nowe JOINy idą bezpośrednio przed nim. NIE zmieniaj metody transformOrderRow() — klucze tablicy pozostają te same. php -l src/Modules/Orders/OrdersRepository.php grep -c "SELECT COUNT\|SELECT COALESCE\|subquery" src/Modules/Orders/OrdersRepository.php # Powinno zwrócić 0 dla wzorców correlated subquery w buildListSql AC-1 satisfied: buildListSql() używa LEFT JOIN zamiast correlated subqueries Task 2: Zamień instance property na static w canResolveMappedMedia() src/Modules/Orders/OrdersRepository.php Znajdź deklarację instance property (okolice klasy): ```php private ?bool $supportsMappedMedia = null; ``` Zmień na: ```php private static ?bool $supportsMappedMedia = null; ``` W metodzie `canResolveMappedMedia()` zmień referencje z `$this->supportsMappedMedia` na `self::$supportsMappedMedia` (we wszystkich miejscach metody — przypisanie i odczyt). To jedyna zmiana. Nie modyfikuj logiki zapytania ani żadnej innej metody. php -l src/Modules/Orders/OrdersRepository.php grep -n "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php # Powinno pokazać: private static ?bool i self::$supportsMappedMedia AC-2 satisfied: canResolveMappedMedia() używa static property Task 3: Migracja z brakującymi indeksami dla tabeli orders database/migrations/20260313_000048_add_orders_performance_indexes.sql Stwórz plik `database/migrations/20260313_000048_add_orders_performance_indexes.sql`. Przed pisaniem migracji, sprawdź jakie indeksy już istnieją: ```bash grep -n "INDEX\|KEY " database/migrations/20260302_000018_create_orders_tables_and_schedule.sql grep -rn "ADD INDEX\|ADD KEY" database/migrations/ | grep -i "order" ``` Migracja powinna dodać brakujące indeksy: ```sql -- Indeksy dla typowych filtrów i sortowań na liście zamówień ALTER TABLE orders ADD INDEX IF NOT EXISTS orders_source_idx (source), ADD INDEX IF NOT EXISTS orders_external_status_idx (external_status_id), ADD INDEX IF NOT EXISTS orders_ordered_at_idx (ordered_at), ADD INDEX IF NOT EXISTS orders_source_status_idx (source, external_status_id); -- Indeks dla allegro status mapping lookup (JOIN w buildListSql) ALTER TABLE allegro_order_status_mappings ADD INDEX IF NOT EXISTS allegro_status_code_idx (allegro_status_code) -- tylko jeśli ten indeks nie istnieje (sprawdź migration 000038) ``` WAŻNE: Sprawdź aktualny schemat przed dodaniem indeksu — nie twórz duplikatów. Użyj `IF NOT EXISTS` gdzie MySQL to obsługuje (MySQL 8.0+), albo sformułuj jako osobne polecenia z IGNORE: `ALTER TABLE orders ADD IGNORE INDEX ...` Sprawdź też `database/migrations/20260308_000038_ensure_order_status_mappings_table.sql` aby wiedzieć jakie indeksy allegro_order_status_mappings już ma. php -l database/migrations/20260313_000048_add_orders_performance_indexes.sql 2>/dev/null || echo "SQL file - no php lint needed" # Sprawdź czy plik istnieje i nie jest pusty cat database/migrations/20260313_000048_add_orders_performance_indexes.sql AC-3 satisfied: migracja z indeksami na source, external_status_id, ordered_at istnieje ## DO NOT CHANGE - Logika filtrowania ($whereSql) — tylko zmiana mechanizmu agregacji, nie filtrów - Metoda transformOrderRow() — zwracane klucze muszą pozostać identyczne - Inne metody OrdersRepository poza buildListSql() i canResolveMappedMedia() - Istniejące pliki migracji — tylko NOWY plik 000048 ## SCOPE LIMITS - Tylko optymalizacja zapytania list; nie zmieniamy zapytań detail/find - Nie dodajemy nowych kolumn ani nie zmieniamy schematu tabel — tylko indeksy - Nie implementujemy cache'owania na poziomie Redis/Memcached — tylko static property Przed zamknięciem planu: - [ ] php -l src/Modules/Orders/OrdersRepository.php — brak błędów - [ ] grep buildListSql src/Modules/Orders/OrdersRepository.php — brak "SELECT COUNT(*) FROM order_items WHERE oi.order_id" - [ ] grep "supportsMappedMedia" src/Modules/Orders/OrdersRepository.php — pokazuje "static" - [ ] Plik migracji 000048 istnieje i jest nieopusty - [ ] Lista zamówień ładuje się bez błędów PHP - 4 correlated subqueries usunięte z buildListSql() - canResolveMappedMedia() z static property - Migracja 000048 z indeksami na source, external_status_id, ordered_at - Zero błędów składniowych PHP Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-01-SUMMARY.md`