Migracje bazy danych (SQLite / D1)
Dokument opisuje, jak schemat bazy danych jest budowany i aktualizowany, oraz jak uruchomić migracje lokalnie i na serwerze VPS (scripts/migrate.mjs). Migracje na Cloudflare D1 (yarn db:update) działają bez zmian do końca migracji na VPS — patrz plan docs/plans/migracja-vps-sqlite-coolify.md.
👤 Instrukcja dla developera
Lokalna baza bez wranglera
-
Zastosuj wszystkie migracje na pustym pliku SQLite:
yarn db:migratePlik powstaje w
./.data/dev.db(katalog jest w.gitignore). Pełny zestaw 282 migracji wykonuje się w około 0,7 s. -
Uruchom aplikację na tym pliku:
APP_RUNTIME=node yarn devnext.config.mjspomija wtedy inicjalizację@opennextjs/cloudflare, agetRuntimeEnv()zwraca bazę i cache z./.data/. Sprawdzenie:curl http://localhost:3000/api/healthodpowiada{"status":"ok","runtime":"node",...}. -
Dane testowe i czyszczenie działają na tej samej bazie:
yarn db:seedyarn db:cleanSkrypty (
scripts/seed-test-data.js,scripts/clean-local-db.js,scripts/seed-camp-registrations.js), fixture Playwright (tests/fixtures/database-setup.ts) oraz serwery deweloperskie hardware (server.ts,scripts/hardware-ws-dev.ts) wybierają plik bazy w jednej kolejności (scripts/lib/local-database.js):DATABASE_PATH, jeśli ustawione,./.data/dev.db, jeśli istnieje lubAPP_RUNTIME=node,- lokalna baza wranglera w
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/— pierwszy plik*.sqlite, który zawiera tabele aplikacji (d1_migrations,game,hardware_device);metadata.sqlitei puste pliki innych bindingów są pomijane.
Bez żadnej bazy skrypty kończą się komunikatem z trzema sposobami jej utworzenia.
Po pobraniu nowych migracji z develop
yarn db:migrate:status # ile migracji czeka
yarn db:migrate # jedna migracja — bez dodatkowych flag
yarn db:migrate --allow-multiple # więcej niż jedna
Runner odmawia zastosowania kilku migracji naraz, gdy baza ma już historię — to ochrona przed przypadkowym przeskokiem wielu wersji na produkcji. Na pustej bazie i w CI (CI=true) flaga nie jest wymagana.
Dodawanie migracji
Zasady numerowania i kolejności są w CLAUDE.md (sekcja Database Migrations). Nowy plik migrations/NNNN_nazwa.sql jest wykrywany automatycznie; sprawdź go lokalnie:
yarn db:migrate --dry-run # pokaże nazwę jako oczekującą
yarn db:migrate # zastosuje; błąd SQL = rollback całego pliku
🛠 Dokumentacja techniczna
scripts/migrate.mjs
Samodzielny skrypt ESM na node:sqlite (bez zależności z node_modules, więc działa w obrazie produkcyjnym po yarn install --production).
| Opcja | Znaczenie |
|---|---|
apply (domyślnie) | stosuje oczekujące migracje |
status | wypisuje liczbę zastosowanych i listę oczekujących, nic nie zmienia |
--database <ścieżka> | plik SQLite; domyślnie DATABASE_PATH albo ./.data/dev.db; katalog nadrzędny jest tworzony |
--migrations-dir <kat.> | domyślnie migrations |
--to <nazwa> | zatrzymuje się po wskazanej migracji; nazwa z .sql lub bez, albo sam numer, jeśli jest jednoznaczny (0265 nie jest) |
--dry-run | jak status |
--allow-multiple | wymagane poza CI, gdy oczekuje więcej niż jedna migracja, a baza ma już zastosowane |
Zachowanie:
- Kolejność — leksykograficzna po pełnej nazwie pliku (tak jak wrangler). W repozytorium istnieją dwa pliki
0265_*, więc sam numer nie jest kluczem. - Tabela historii —
d1_migrations(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL), identyczna z tabelą tworzoną przezwrangler d1 migrations apply. Baza wyeksportowana z D1 od razu „wie”, które migracje ma, a runner stosuje tylko brakujące. - Transakcja na plik —
BEGIN IMMEDIATE→exec(plik)→INSERT INTO d1_migrations→COMMIT; każdy błąd toROLLBACKcałego pliku, wpis w historii nie powstaje, kod wyjścia 1.BEGIN IMMEDIATEblokuje zapis, więc dwa równoległe runnery (np. dwa kontenery przy deployu) nie wejdą sobie w drogę — drugi czeka dobusy_timeout(5 s). - PRAGMA w plikach —
PRAGMA foreign_keys = OFFwewnątrz transakcji jest w SQLite no-opem (tak samo było na D1);PRAGMA defer_foreign_keys = ONdziała i jest używane przez migracje przebudowujące tabele (np. 0137, 0262). Po pełnym przebieguPRAGMA foreign_key_checkzwraca 0 wierszy,integrity_check=ok. - Triggery — ciała
CREATE TRIGGER … BEGIN … ENDsą wykonywane przezexec()całego pliku, więc wielowyrażeniowe triggery (26 plików) nie wymagają dzielenia SQL na instrukcje. - Połączenie —
journal_mode = WAL,busy_timeout = 5000,foreign_keys = ON(te same ustawienia colib/runtime/sqlite.ts).
Wybór lokalnej bazy — scripts/lib/local-database.js
resolveLocalDatabasePath({ cwd, env }) zwraca ścieżkę albo null; describeMissingDatabase() zwraca komunikat z instrukcją. Moduł jest CommonJS, żeby działał zarówno w skryptach node scripts/*.js, jak i w plikach TypeScript (server.ts, fixture Playwright) przez tsx/Playwright.
Skrypty package.json
| Skrypt | Runtime | Polecenie |
|---|---|---|
yarn db:update | Cloudflare | wrangler d1 migrations apply DB (używane przez scripts/build-and-migrate.sh z --remote; znika w fazie 6) |
yarn db:migrate | Node | node scripts/migrate.mjs |
yarn db:migrate:status | Node | node scripts/migrate.mjs status |
yarn db:add-migration | oba | wrangler d1 migrations create DB |
Na VPS entrypoint kontenera web uruchamia node dist/migrate.js (kopia pre-deploy VACUUM INTO przed nim) — opis w planie migracji, pkt 4.11–4.12.
Testy
__tests__/scripts/migrate.test.ts uruchamia runner jako proces potomny na tymczasowym katalogu migracji (kolejność, tabela zgodna z wranglerem, rollback, --to, --dry-run, --allow-multiple, CI) oraz stosuje pełny zestaw z migrations/ na pustym pliku i sprawdza foreign_key_check / integrity_check. __tests__/scripts/local-database.test.ts pokrywa kolejność wyboru bazy.